Compare commits

...

226 Commits

Author SHA1 Message Date
be55bfd1e8 Increase version to 0.5.0-rc1 ()
* Increase version to 0.5.0-rc1

* Increment version to 0.5.0-rc1 in cloudbuild.yaml
2019-04-22 18:09:36 -07:00
8389a62cf1 Release Process for Open Match () 2019-04-22 14:12:02 -07:00
af8895e629 Remove knative link because it fails in tests. () 2019-04-22 11:47:51 -07:00
2a3241307f Properly set tag and repository when making install/yaml/ () 2019-04-22 11:13:03 -07:00
f777a4f407 Publish all install/yaml/*.yaml files. ()
* Publish all install/yaml/*.yaml files.

* Update instructions and add publish post commit.

* Add yaml/
2019-04-22 10:02:49 -07:00
88ca8d7b7c DOcumentation () 2019-04-21 17:23:54 -07:00
3a09ce142a Fix namespace issues in example yaml () 2019-04-19 17:03:56 -07:00
8d8fdf0494 Add vanity url redirection support. () 2019-04-19 16:33:00 -07:00
45b0a7c38e Remove deprecated examples, evaluator and mmforc () 2019-04-19 15:34:21 -07:00
4cbee9d8a7 Remove deprecated artifacts from build pipeline () 2019-04-19 14:46:49 -07:00
55afac2c93 Embed profile config in the container to be used for standalone executions. ()
Embed profile config in the container to be used for standalone executions. Will create a separate issue to figure out a better way to do this.
2019-04-19 14:07:41 -07:00
8077dbcdba Changes to make the demo steps easier () 2019-04-19 11:30:30 -07:00
f1fc02755b Update theme and logo for Open Match website () 2019-04-19 11:01:31 -07:00
0cce1745bc Changes to Backend API and Backend Client to support GRPC Function Ha… ()
* Changes to Backend API and Backend Client to support GRPC Function Harness
2019-04-19 10:41:15 -07:00
d57b5f1872 Helm chart changes to not install mmforc and deploy function Service () ()
* Helm chart changes to not install mmforc and deploy function Service
2019-04-19 10:17:06 -07:00
1355e5c79e Fix lint issues in helm chart and improve lint coverage. () 2019-04-19 09:49:42 -07:00
4809f2801f Add Open Match Logo () 2019-04-19 08:28:13 -07:00
68d323f3ea 2nd pass of lint errors. () 2019-04-19 05:42:57 -07:00
b99160e356 Fix grpc harness startup panic due to http proxy not being set up () 2019-04-18 20:02:04 -07:00
98d4c31c61 Fix most of the lint errors from golangci. () 2019-04-18 18:15:46 -07:00
b4beb68920 Reduce log spam in backendapi () 2019-04-18 15:39:41 -07:00
b41a704886 Bump versions of dependencies () 2019-04-18 14:12:05 -07:00
88a692cdf3 Evaluator Harness and Sample golang Evaluator ()
* Evaluator Harness and sample serving Evaluator
2019-04-18 12:35:37 -07:00
623519bbb4 Core Logic for the GRPC Harness () ()
Core Logic for the MatchFunction GRPC Harness
2019-04-18 12:16:38 -07:00
655abfbb26 Example MMF demonstrating the use of the GRPC harness () 2019-04-18 10:10:06 -07:00
ac81b74fad Add Kaniko build cache ()
* Add Kaniko build cache - partly resolves 
2019-04-18 00:30:02 -07:00
ba62520d9c Prevent sudo on Makefile for commands that require auth. () 2019-04-17 20:58:16 -07:00
0205186e6f Remove install/yaml/ it will be moved to release artifacts. ()
* Remove install/yaml/ it will be moved to release artifacts.

* Add the ignore files.

* Create install/yaml/ directory for output targets.
2019-04-17 17:50:39 -07:00
ef2b1ea0a8 Implement REST proxy initializations and modified tests accordingly ()
This commit resolves  and generates swagger.json files for API visualization
2019-04-17 17:28:36 -07:00
1fe2bd4900 Add 'make presubmit' to keep generated files up to date. () 2019-04-17 17:04:05 -07:00
5333ef2092 Enable cloudbuild dev site to fix local cloud build error () 2019-04-17 16:17:01 -07:00
09b727b555 Remove the deprecated deployment mechanism for openmatch components () 2019-04-17 15:45:38 -07:00
c542d6d1c3 Serving GRPC Harness and example MMF scaffolding () ()
* Serving GRPC Harness and example MMF scaffolding

* Serving GRPC Harness and example MMF scaffolding

* Update logger field to add function name

* Update harness to use the TCP listener
2019-04-17 14:57:01 -07:00
8f3f7625ec Increases paralellism of the build () 2019-04-17 13:07:39 -07:00
6a4f309bd5 Remove temp files. () 2019-04-17 12:41:38 -07:00
26f5426b61 Disable logrus.SetReportCaller() () 2019-04-17 12:26:43 -07:00
f464b0bd7b Fix port allocation race condition during tests. () 2019-04-17 11:54:56 -07:00
092b7e634c Move GOPROXY=off to CI only. () 2019-04-17 11:01:37 -07:00
454a3d6cca Bump required Go version because of a dependency. () 2019-04-15 20:24:09 -07:00
50e3ede4b9 Remove use of GOPATH from Makefile () 2019-04-15 16:19:31 -07:00
6c36145e9b Mini Match () 2019-04-12 16:16:42 -07:00
47644004db Add link tests for website and removed broken links. () 2019-04-12 15:26:32 -07:00
1dec4d7555 Unify gRPC server initialization () 2019-04-12 12:47:27 -07:00
1c6f43a95f Add a link to the build queue. () 2019-04-12 11:38:50 -07:00
0cea4ed713 Add temporary redirect site for Open Match () 2019-04-12 11:24:23 -07:00
db912b2d68 Add reduced permissions for mmforc service account. () 2019-04-12 10:25:19 -07:00
726b1d4063 CI with Makefile () 2019-04-12 07:51:10 -07:00
468aef3835 Ignore files for Mini Match. () 2019-04-11 15:16:26 -07:00
c6e257ae76 Unified gRPC server initialization ()
* Unified gRPC server initialization

* Fix closure and review feedback
2019-04-11 15:06:07 -07:00
8e071020fa Kubernetes YAML configs for Open Match. () 2019-04-11 14:28:27 -07:00
c032e8f382 Detect sudo invocations to Makefile () 2019-04-11 14:09:52 -07:00
2af432c3d7 Fix build artifacts
Fix build artifacts issue 
2019-04-11 13:23:44 -07:00
4ddceb62ee fixed bugs in py3 mmf ()
fix py3 mmf image
2019-04-11 06:32:59 -07:00
ddb4521444 Add license preamble to proto and dockerfiles. () 2019-04-10 20:24:31 -07:00
86918e69eb Replace CURDIR with REPOSITORY ROOT 2019-04-10 16:32:01 -07:00
2d6db7e546 Remove manual stats that ocgrpc interceptor already records. 2019-04-10 16:21:42 -07:00
fc52ef6428 REST Implementation 2019-04-10 15:34:49 -07:00
1bfb30be6f Fix redis connection bugs and segfault in backendclient. () 2019-04-10 13:27:41 -07:00
9ee341baf2 Move configs from backendclient image to ConfigMap. () 2019-04-10 12:59:12 -07:00
7869e6eb81 Add opencensus metrics for Redis 2019-04-10 12:36:35 -07:00
7edca56f56 Disable php-proto building since it's missing gRPC client 2019-04-10 10:06:42 -07:00
eaedaa0265 Split up README.md and add project logo. 2019-04-10 08:26:21 -07:00
9cc8312ff5 Rename Function to MatchFunction and modify related protos () 2019-04-10 08:15:40 -07:00
2f0a1ad05b updating app.yaml 2019-04-09 20:47:33 -07:00
2ff77ac90b Fix 'make create-gke-cluster' ()
It is missing a dash on one of the arguments, which breaks things.
2019-04-09 15:59:16 -07:00
2a3cfea505 Add base package file for godoc index and go get. 2019-04-09 14:16:54 -07:00
b8326c2a91 Fix build dependencies to build/site/ 2019-04-09 14:05:03 -07:00
ccc9d87692 Disable the PHP example during the CI build. 2019-04-09 12:01:34 -07:00
bba49f3ec4 Simplify the go package path for proto definitions 2019-04-09 11:41:29 -07:00
632157806f Remove symlinks to config files because they are mounted via ConfigMaps. 2019-04-09 11:11:36 -07:00
6e039cb797 Delete images and scripts obsoleted by Makefile. 2019-04-09 10:40:53 -07:00
8db062f7b9 Use Request/Response protos in gRPC servers. 2019-04-03 21:11:42 -07:00
f379a5eb46 Disable 'Lint: Kubernetes Configs'
It is currently failing.
2019-04-03 18:28:24 -07:00
f3160cfc5c generate install.yaml with Helm
fixed helm templates

changes in helm templates

adding redis auth to the helm chart

helm templates changes

makefile: gen-install

make set-redis-password

make gen-install

fixing indentation in Makefile

remove old redis installation

use public images in install/yaml/

remove helm chart meta from static install yaml files

fixing cloudbuild

remove helm chart meta from static install yaml files

workaround for broken om-configmap data formatting

make gen-prometheus-install

drop namespace in OM resources definitions

override default matchmaker_config at Helm chart installation

fixed Makefile after rebase

matchmaker config: use latest public images

1) install Redis in same namespace with Open-match;2) Making namespace and Helm release names consistent in all places
2019-04-03 13:40:13 -07:00
442a1ff013 Update dependencies and resolve issue 2019-04-02 20:21:14 -07:00
0fb75ab35e Delete old cloudbuild.yaml files, obsoleted by PR 2019-04-02 11:23:14 -07:00
6308b218cc Minimize dependency on Viper and make config read-only. 2019-04-02 07:46:18 -07:00
624ba5c018 [charts/open-match] fix mmlogicapi service selector 2019-04-01 18:10:15 -07:00
82d034f8e4 Fix dependency issues in the build. 2019-04-01 11:05:57 -07:00
97eed146da update protoc version to 3.7.1
This fixes the bug outlined here https://github.com/protocolbuffers/protobuf/issues/5875
2019-04-01 09:49:19 -07:00
6dd23ff6ad Merge pull request from jeremyje/master
Merge 040wip into master.
2019-03-29 14:29:22 -07:00
03c7db7680 Merge 040wip 2019-03-28 11:12:07 -07:00
e5538401f6 Update protobuf definitions 2019-03-26 17:45:52 -07:00
eaa811f9ac Add example helm chart, replace example dashboard. 2019-03-26 17:45:28 -07:00
3b1c6b9141 Merge 2019-03-26 15:26:17 -07:00
34f9eb9bd3 Building again 2019-03-26 12:31:19 -07:00
3ad7f75fb4 Attempt to fix the build 2019-03-26 12:31:19 -07:00
78bd48118d Tweaks 2019-03-26 12:31:19 -07:00
3e71894111 Merge 2019-03-26 12:31:19 -07:00
36decb4068 Merge 2019-03-26 12:31:19 -07:00
f79b782a3a Go Modules 2019-03-26 11:14:48 -07:00
db186e55ff Move Dockfiles to build C#, Golang, PHP, and Python3 MMFs. 2019-03-26 09:54:10 -07:00
957465ce51 Remove dead code that was moved to internal/app/mmlogicapi/apisrv/ 2019-03-25 16:14:25 -07:00
478eb61589 Delete unnecessary copy of protos in frontendclient. 2019-03-25 16:13:56 -07:00
6d2a5b743b Remote executable bit from files that are not executable. 2019-03-13 09:31:24 -07:00
9c943d5a10 Fix comment 2019-03-12 22:04:42 -07:00
8293d44ee0 Fix typos in comments, set and playerindices 2019-03-12 22:04:42 -07:00
a3bd862e76 store optional Redis password inside the Secret 2019-03-12 21:52:59 -07:00
c424d5eac9 Update .gcloudignore to include .gitignore's filters so that Cloud Build packages don't upload binaries. 2019-03-11 16:29:50 +09:00
2e6f5173e0 Add Prometheus service discovery annotations to the Open Match servers. 2019-03-11 16:25:21 +09:00
ee4bba44ec Makefile for simpler development 2019-03-11 16:14:00 +09:00
8e923a4328 Use grpc error codes for responses. 2019-03-11 16:13:06 +09:00
52efa04ee6 Add RPC dashboard and instructions to add more dashboards. 2019-03-07 10:58:53 -08:00
67d4965648 Helm charts for open-match, prometheus, and grafana 2019-03-06 17:09:09 -08:00
7a7b1cb305 Open Match CI support via Cloud Build 2019-03-04 09:41:19 -08:00
377a9621ff Improve error handling of Redis open connection failures. 2019-02-27 19:35:23 -08:00
432dd5a504 Consolidate Ctrl+Break handling into it's own go package. 2019-02-27 17:52:58 +01:00
7446f5b1eb Move out Ctrl+Break wait signal to it's own package. 2019-02-27 17:52:58 +01:00
15ea999628 Remove init() methods from OM servers since they aren't needed. 2019-02-27 08:58:39 +01:00
b5367ea3aa Add config/ in the search path for configuration so that PWD/config can be used as a ConfigMap mount path. 2019-02-25 16:49:35 -08:00
e022c02cb6 golang mmf serving harness 2019-02-25 04:54:02 -05:00
a13455d5b0 Move application logic from cmd/ to internal/app/ 2019-02-24 13:56:48 +01:00
16741409e7 Cleaner builds using svn for github 2019-02-19 09:24:50 -05:00
d7e8f8b3fa Testing 2019-02-19 07:30:26 -05:00
8c97c8f141 Testing2 2019-02-19 07:26:11 -05:00
6a8755a13d Testing 2019-02-19 07:24:10 -05:00
4ed6d275a3 remove player from ignorelists on frontend.DeletePlayer call 2019-02-19 20:01:29 +09:00
cb49eb8646 Merge remote-tracking branch 'origin/calebatwd/knative-rest-mmf' into 040wip 2019-02-16 04:01:01 -05:00
a7458dabf2 Fix test/example paths 2019-02-14 10:56:33 +09:00
5856b7d873 Merge branch '040wip' of https://github.com/GoogleCloudPlatform/open-match into 040wip 2019-02-11 01:23:06 -05:00
7733824c21 Remove matchmaking config file from base image 2019-02-11 01:22:23 -05:00
f1d261044b Add function port to config 2019-02-11 01:21:28 -05:00
95820431ab Update dev instuctions 2019-02-11 01:20:55 -05:00
0002ecbdb2 Review feedback. 2019-02-09 15:28:48 +09:00
2eb51b5270 Fix build and test breakages 2019-02-09 15:28:48 +09:00
1847f79571 Convert JSON k8s deployment configs to YAML. 2019-02-09 15:17:22 +09:00
58ff12f3f8 Add stackdriver format support via TV4/logrus-stackdriver-formatter. Simply set format in config to stackdriver 2019-02-09 15:14:00 +09:00
b0b7b4bd15 Update git ignore to ignore goland ide files 2019-02-09 15:09:00 +09:00
f3f1f36099 Comment type 2019-02-08 14:21:36 -08:00
f8cfb1b90f Add rest call support to job scheduling. This is a prototype implementation to support knative experimentation. 2019-02-08 14:20:29 -08:00
393e1d6de2 added configurable backoff to MatchObject and Player watchers 2019-02-08 16:19:52 +09:00
a11556433b Merge branch 'master' into 040wip 2019-02-08 01:48:54 -05:00
3ee9c05db7 Merge upstream changes 2019-02-08 01:47:43 -05:00
de7ba2db6b added demo attr to player indices 2019-02-03 20:17:13 -08:00
8393454158 fixes for configmap 2019-02-03 20:17:13 -08:00
6b93ac7663 configmap for matchmaker config 2019-02-03 20:17:13 -08:00
fe2410e9d8 PHP MMF: move cfg values to env vars 2019-02-03 20:17:13 -08:00
d8ecf1c439 doc update 2019-02-03 20:17:13 -08:00
8577f6bd4d Move cfg values to env vars for MMFs 2019-02-03 20:17:13 -08:00
470be06d16 fixed set.Difference() 2019-01-29 22:38:18 -08:00
c6e4dae79b fix google cloud knative url 2019-01-25 11:38:46 -08:00
23f83eddd1 mmlogic GetPlayerPool bugfix 2019-01-23 19:57:36 -05:00
dd794fd004 py3 mmf empty pools bugfix 2019-01-23 19:57:16 -05:00
f234433e33 write to error if all pools are empty in py3 mmf 2019-01-23 19:57:16 -05:00
d52773543d check for empty pools in py3 mmf 2019-01-23 19:57:16 -05:00
bd4ab0b530 mmlogic GetPlayerPool bugfix 2019-01-23 14:18:00 +03:00
6b9cd11be3 fix py3 mmf 2019-01-16 18:01:10 +03:00
1443bd1e80 PHP MMF: move cfg values to env vars 2019-01-16 13:41:44 +03:00
3fd8081dc5 doc update 2019-01-15 11:58:42 -05:00
dda949a6a4 Move cfg values to env vars for MMFs 2019-01-15 11:25:02 -05:00
128f0a2941 Merge branch 'master' of https://github.com/GoogleCloudPlatform/open-match 2019-01-15 09:42:01 -05:00
5f8a57398a Fix cloud build issue caused by 5f827b5c7c81c79ef9341cbebb51880f74b78a35 2019-01-15 09:41:38 -05:00
327d64611b This time with working hyperlink 2019-01-14 23:44:10 +09:00
5b4cdce610 Bump version number 2019-01-14 23:43:11 +09:00
56e08e82d4 Revert accidental file type change 2019-01-14 09:32:13 -05:00
2df027c9f6 Bold release numbers 2019-01-10 00:28:31 -05:00
913af84931 Use public repo URL 2019-01-09 02:18:53 -05:00
de6064f9fd Use public repo URL 2019-01-09 02:18:22 -05:00
867c55a409 Fix registry URL and add symlink issue 2019-01-09 02:15:11 -05:00
36420be2ce Revert accidental removal of symlink 2019-01-09 02:14:32 -05:00
16e9dda64a Bugfix for no commandline args 2019-01-09 02:14:07 -05:00
1ef9a896bf Revert accidental commit of empty file 2019-01-09 02:13:30 -05:00
75f2b84ded Up default timeout 2019-01-09 02:03:47 -05:00
2268baf1ba revert accidential commit of local change 2019-01-09 02:00:36 -05:00
9e43d989ea Remove debug sleep command 2019-01-09 00:10:47 -05:00
869725baee Bump k8s version 2019-01-08 23:56:07 -05:00
ae26ac3cd3 Merge remote-tracking branch 'origin/master' into 030wip 2019-01-08 23:41:55 -05:00
826af77396 Point to public registry and update tag 2019-01-08 23:37:38 -05:00
294d03e18b Roadmap 2019-01-08 22:39:08 -05:00
b27116aedd 030 RC2 2019-01-08 02:19:53 -05:00
074c0584f5 030 RC1 issue thread updates https://github.com/GoogleCloudPlatform/open-match/pull/55 2019-01-07 23:35:42 -05:00
210e00703a production guide now has placeholder notes, low hanging fruit 2019-01-07 23:35:14 -05:00
3ffbddbdd8 Updates to add optional TTL to redis objects 2019-01-05 23:37:38 -05:00
5f827b5c7c doesn't work 2019-01-05 23:01:33 -05:00
a161e6dba9 030 WIP first pass 2018-12-30 05:31:49 -05:00
7e70683d9b fix broken sed command 2018-12-30 04:34:27 -05:00
38bd94c078 Merge NoFr1ends commit 6a5dc1c 2018-12-30 04:16:48 -05:00
83366498d3 Update Docs 2018-12-30 03:45:39 -05:00
929e089e4d rename api call 2018-12-30 03:35:25 -05:00
a6b56b19d2 Merge branch to address issue 2018-12-28 04:01:59 -05:00
c2b6fdc198 Updates to FEClient and protos 2018-12-28 02:48:03 -05:00
43a4f046f0 Update config 2018-12-27 03:14:40 -05:00
b79bc2591c Remove references to connstring 2018-12-27 03:07:26 -05:00
61198fd168 No unused code 2018-12-27 03:04:18 -05:00
c1dd3835fe Updated logging 2018-12-27 02:55:16 -05:00
f3c9e87653 updates to documentation and builds 2018-12-27 02:28:43 -05:00
0064116c34 Further deletion and fix indexing for empty fields 2018-12-27 02:09:20 -05:00
298fe18f29 Updates to player deletion logic, metadata indices 2018-12-27 01:27:39 -05:00
6c539ab2a4 Remove manual filenames in logs 2018-12-26 07:43:54 -05:00
b6c59a7a0a Player watcher for FEAPI brought over from Doodle 2018-12-26 07:29:28 -05:00
f0536cedde Merge Ilya's updates 2018-12-26 00:18:00 -05:00
48fa4ba962 Update Redis HA details 2018-12-25 23:58:54 -05:00
39ff99b65e rename 'redis-sentinel' to just 'redis' 2018-12-26 13:51:24 +09:00
78c7b3b949 redis failover deployment 2018-12-26 13:51:24 +09:00
6a5dc1c508 Fix typo in development guide 2018-12-26 13:49:54 +09:00
9f84ec9bc9 First pass. Works but hacky. 2018-12-25 23:47:30 -05:00
e48b7db56f Fix parsing of empty matchobject fields 2018-12-26 13:45:40 +09:00
bffd54727c Merge branch 'udptest' into test_agones 2018-12-19 02:59:04 -05:00
ab90f5f6e0 got udp test workign 2018-12-19 02:56:20 -05:00
632415c746 simple udp client & server to integrate with agones 2018-12-18 23:58:02 +03:00
0882c63eb1 Update messages; more redis code sequestered to redis module 2018-12-16 08:12:42 -05:00
ee6716c60e Merge PL 47 2018-12-15 23:56:35 -05:00
bb5ad8a596 Merge 951bc8509d5eb8fceb138135c001c6a7b7f9bb25 into 275fa2d125e91fd25981124387f6388431f73874 2018-12-15 19:32:28 +00:00
951bc8509d Remove strings import as it's no longer used 2018-12-15 14:11:31 -05:00
ab8cd21633 Update to use Xid instead of UUID. 2018-12-15 14:11:05 -05:00
721cd2f7ae Still needs make file or the like and updated instructions 2018-12-10 14:05:00 +09:00
13cd1da631 Merge remote-tracking branch 'origin/json-logging' into feupdate 2018-12-06 23:28:35 -05:00
275fa2d125 Awkward wording 2018-12-07 13:17:39 +09:00
4a8e018599 Fix merge conflict 2018-12-06 22:04:52 -05:00
c1b5d44947 Update current version number 2018-12-06 22:01:14 -05:00
ae9db9fae8 Merge remote-tracking branch 'origin/master' 2018-12-06 21:56:43 -05:00
104fbd19cd Header level tweaks 2018-12-06 02:54:40 -05:00
3b2571fced Doc updates for 0.2.0 2018-12-06 02:53:16 -05:00
486c64798b Merge tag '020rc2' into feupdate 2018-12-06 02:14:58 -05:00
3fb17c5f22 Merge remote-tracking branch 'origin/master' into 020rc2 2018-12-06 02:12:55 -05:00
3f42e3d986 Finalizing 0.2.0 updates to dev doc 2018-12-06 01:16:26 -05:00
0c74debbb3 Updated docs for 0.2.0 2018-12-05 03:59:57 -05:00
1854ee0ba1 Fix formatting 2018-12-04 01:07:31 -05:00
99d9d7e2b5 Update for 0.2.0 Release 2018-12-02 21:48:48 -05:00
52f9e2810f WIP indexing 2018-11-28 04:10:08 -05:00
db60d7ac5f Merge from 0.2.0 2018-11-28 02:23:26 -05:00
3fcedbf13b Remove enum status states. No justification yet. 2018-11-26 17:42:08 -08:00
274edaae2e Grpc code for calling functions in mmforc 2018-11-26 17:40:25 -08:00
8ed865d300 Initial function messages plus protoc regen 2018-11-26 17:05:42 -08:00
4e0bb5c07d Add DGS (Dedicated Game Server) to glossary 2018-11-20 14:10:41 +09:00
326dd6c6dd Add logging config to support json and level selection for logrus 2018-11-17 16:11:33 -08:00
8470 changed files with 353482 additions and 10722 deletions
.dockerignore.gcloudignore.gitignoreCHANGELOG.mdDockerfile.backendapiDockerfile.baseDockerfile.base-buildDockerfile.ciDockerfile.evaluatorDockerfile.frontendapiDockerfile.mmf_goDockerfile.mmf_phpDockerfile.mmf_py3Dockerfile.mmforcDockerfile.mmlogicapiMakefileREADME.mdRELEASE.md
api
cloudbuild.yamlcloudbuild_backendapi.yamlcloudbuild_base.yamlcloudbuild_evaluator.yamlcloudbuild_frontendapi.yamlcloudbuild_mmf_go.yamlcloudbuild_mmf_php.yamlcloudbuild_mmf_py3.yamlcloudbuild_mmforc.yamlcloudbuild_mmlogicapi.yaml
cmd
config
deployments/k8s
doc.go
docs
examples
go.modgo.sum
install
internal
site
.gcloudignoreapp.yamlappengine.go
assets/scss
config.toml
content/en
handler.gohandler_test.gohtmltest.yaml
layouts
redirect
static
themes/docsy
.s3deploy.ymlCONTRIBUTING.mdLICENSEREADME.md
assets
icons
js
scss
vendor
Font-Awesome
.github
CHANGELOG.mdCODE_OF_CONDUCT.mdCONTRIBUTING.mdLICENSE.txtREADME.mdUPGRADING.md
advanced-options
metadata
raw-svg
brands
500px.svgaccessible-icon.svgaccusoft.svgadn.svgadversal.svgaffiliatetheme.svgalgolia.svgamazon-pay.svgamazon.svgamilia.svgandroid.svgangellist.svgangrycreative.svgangular.svgapp-store-ios.svgapp-store.svgapper.svgapple-pay.svgapple.svgasymmetrik.svgaudible.svgautoprefixer.svgavianex.svgaviato.svgaws.svgbandcamp.svgbehance-square.svgbehance.svgbimobject.svgbitbucket.svgbitcoin.svgbity.svgblack-tie.svgblackberry.svgblogger-b.svgblogger.svgbluetooth-b.svgbluetooth.svgbtc.svgburomobelexperte.svgbuysellads.svgcc-amazon-pay.svgcc-amex.svgcc-apple-pay.svgcc-diners-club.svgcc-discover.svgcc-jcb.svgcc-mastercard.svgcc-paypal.svgcc-stripe.svgcc-visa.svgcentercode.svgchrome.svgcloudscale.svgcloudsmith.svgcloudversify.svgcodepen.svgcodiepie.svgconnectdevelop.svgcontao.svgcpanel.svgcreative-commons-by.svgcreative-commons-nc-eu.svgcreative-commons-nc-jp.svgcreative-commons-nc.svgcreative-commons-nd.svgcreative-commons-pd-alt.svgcreative-commons-pd.svgcreative-commons-remix.svgcreative-commons-sa.svgcreative-commons-sampling-plus.svgcreative-commons-sampling.svgcreative-commons-share.svgcreative-commons.svgcss3-alt.svgcss3.svgcuttlefish.svgd-and-d.svgdashcube.svgdelicious.svgdeploydog.svgdeskpro.svgdeviantart.svgdigg.svgdigital-ocean.svgdiscord.svgdiscourse.svgdochub.svgdocker.svgdraft2digital.svgdribbble-square.svgdribbble.svgdropbox.svgdrupal.svgdyalog.svgearlybirds.svgebay.svgedge.svgelementor.svgember.svgempire.svgenvira.svgerlang.svgethereum.svgetsy.svgexpeditedssl.svgfacebook-f.svgfacebook-messenger.svgfacebook-square.svgfacebook.svgfirefox.svgfirst-order-alt.svgfirst-order.svgfirstdraft.svgflickr.svgflipboard.svgfly.svgfont-awesome-alt.svgfont-awesome-flag.svgfont-awesome-logo-full.svgfont-awesome.svgfonticons-fi.svgfonticons.svgfort-awesome-alt.svgfort-awesome.svgforumbee.svgfoursquare.svgfree-code-camp.svgfreebsd.svgfulcrum.svggalactic-republic.svggalactic-senate.svgget-pocket.svggg-circle.svggg.svggit-square.svggit.svggithub-alt.svggithub-square.svggithub.svggitkraken.svggitlab.svggitter.svgglide-g.svgglide.svggofore.svggoodreads-g.svggoodreads.svggoogle-drive.svggoogle-play.svggoogle-plus-g.svggoogle-plus-square.svggoogle-plus.svggoogle-wallet.svggoogle.svggratipay.svggrav.svggripfire.svggrunt.svggulp.svghacker-news-square.svghacker-news.svghips.svghire-a-helper.svghooli.svghornbill.svghotjar.svghouzz.svghtml5.svghubspot.svgimdb.svginstagram.svginternet-explorer.svgioxhost.svgitunes-note.svgitunes.svgjava.svgjedi-order.svgjenkins.svgjoget.svgjoomla.svgjs-square.svgjs.svgjsfiddle.svgkeybase.svgkeycdn.svgkickstarter-k.svgkickstarter.svgkorvue.svglaravel.svglastfm-square.svglastfm.svgleanpub.svgless.svgline.svglinkedin-in.svglinkedin.svglinode.svglinux.svglyft.svgmagento.svgmailchimp.svgmandalorian.svgmastodon.svgmaxcdn.svgmedapps.svgmedium-m.svgmedium.svgmedrt.svgmeetup.svgmegaport.svgmicrosoft.svgmix.svgmixcloud.svgmizuni.svgmodx.svgmonero.svgnapster.svgnimblr.svgnintendo-switch.svgnode-js.svgnode.svgnpm.svgns8.svgnutritionix.svgodnoklassniki-square.svgodnoklassniki.svgold-republic.svgopencart.svgopenid.svgopera.svgoptin-monster.svgosi.svgpage4.svgpagelines.svgpalfed.svgpatreon.svgpaypal.svgperiscope.svgphabricator.svgphoenix-framework.svgphoenix-squadron.svgphp.svgpied-piper-alt.svgpied-piper-hat.svgpied-piper-pp.svgpied-piper.svgpinterest-p.svgpinterest-square.svgpinterest.svgplaystation.svgproduct-hunt.svgpushed.svgpython.svgqq.svgquinscape.svgquora.svgr-project.svgr.svgravelry.svgreact.svgreadme.svgrebel.svgred-river.svgreddit-alien.svgreddit-square.svgreddit.svgrendact.svgrenren.svgreplyd.svgresearchgate.svgresolving.svgrev.svgrocketchat.svgrockrms.svgsafari.svgsass.svgschlix.svgscribd.svgsearchengin.svgsellcast.svgsellsy.svgservicestack.svgshirtsinbulk.svgshopware.svgsimplybuilt.svgsistrix.svgsith.svgskyatlas.svgskype.svgslack-hash.svgslack.svgslideshare.svgsnapchat-ghost.svgsnapchat-square.svgsnapchat.svgsoundcloud.svgspeakap.svgspotify.svgsquarespace.svgstack-exchange.svgstack-overflow.svgstaylinked.svgsteam-square.svgsteam-symbol.svgsteam.svgsticker-mule.svgstrava.svgstripe-s.svgstripe.svgstudiovinari.svgstumbleupon-circle.svgstumbleupon.svgsuperpowers.svgsupple.svgteamspeak.svgtelegram-plane.svgtelegram.svgtencent-weibo.svgthemeco.svgthemeisle.svgtrade-federation.svgtrello.svgtripadvisor.svgtumblr-square.svgtumblr.svgtwitch.svgtwitter-square.svgtwitter.svgtypo3.svguber.svguikit.svguniregistry.svguntappd.svgusb.svgussunnah.svgvaadin.svgviacoin.svgviadeo-square.svgviadeo.svgviber.svgvimeo-square.svgvimeo-v.svgvimeo.svgvine.svgvk.svgvnv.svgvuejs.svgweebly.svgweibo.svgweixin.svgwhatsapp-square.svgwhatsapp.svgwhmcs.svgwikipedia-w.svgwindows.svgwix.svgwolf-pack-battalion.svgwordpress-simple.svgwordpress.svgwpbeginner.svgwpexplorer.svgwpforms.svgxbox.svgxing-square.svgxing.svgy-combinator.svgyahoo.svgyandex-international.svgyandex.svgyelp.svgyoast.svgyoutube-square.svgyoutube.svg
regular
address-book.svgaddress-card.svgangry.svgarrow-alt-circle-down.svgarrow-alt-circle-left.svgarrow-alt-circle-right.svgarrow-alt-circle-up.svgbell-slash.svgbell.svgbookmark.svgbuilding.svgcalendar-alt.svgcalendar-check.svgcalendar-minus.svgcalendar-plus.svgcalendar-times.svgcalendar.svgcaret-square-down.svgcaret-square-left.svgcaret-square-right.svgcaret-square-up.svgchart-bar.svgcheck-circle.svgcheck-square.svgcircle.svgclipboard.svgclock.svgclone.svgclosed-captioning.svgcomment-alt.svgcomment-dots.svgcomment.svgcomments.svgcompass.svgcopy.svgcopyright.svgcredit-card.svgdizzy.svgdot-circle.svgedit.svgenvelope-open.svgenvelope.svgeye-slash.svgeye.svgfile-alt.svgfile-archive.svgfile-audio.svgfile-code.svgfile-excel.svgfile-image.svgfile-pdf.svgfile-powerpoint.svgfile-video.svgfile-word.svgfile.svgflag.svgflushed.svgfolder-open.svgfolder.svgfont-awesome-logo-full.svgfrown-open.svgfrown.svgfutbol.svggem.svggrimace.svggrin-alt.svggrin-beam-sweat.svggrin-beam.svggrin-hearts.svggrin-squint-tears.svggrin-squint.svggrin-stars.svggrin-tears.svggrin-tongue-squint.svggrin-tongue-wink.svggrin-tongue.svggrin-wink.svggrin.svghand-lizard.svghand-paper.svghand-peace.svghand-point-down.svghand-point-left.svghand-point-right.svghand-point-up.svghand-pointer.svghand-rock.svghand-scissors.svghand-spock.svghandshake.svghdd.svgheart.svghospital.svghourglass.svgid-badge.svgid-card.svgimage.svgimages.svgkeyboard.svgkiss-beam.svgkiss-wink-heart.svgkiss.svglaugh-beam.svglaugh-squint.svglaugh-wink.svglaugh.svglemon.svglife-ring.svglightbulb.svglist-alt.svgmap.svgmeh-blank.svgmeh-rolling-eyes.svgmeh.svgminus-square.svgmoney-bill-alt.svgmoon.svgnewspaper.svgobject-group.svgobject-ungroup.svgpaper-plane.svgpause-circle.svgplay-circle.svgplus-square.svgquestion-circle.svgregistered.svgsad-cry.svgsad-tear.svgsave.svgshare-square.svgsmile-beam.svgsmile-wink.svgsmile.svgsnowflake.svgsquare.svgstar-half.svgstar.svgsticky-note.svgstop-circle.svgsun.svgsurprise.svgthumbs-down.svgthumbs-up.svgtimes-circle.svgtired.svgtrash-alt.svguser-circle.svguser.svgwindow-close.svgwindow-maximize.svgwindow-minimize.svgwindow-restore.svg
solid
address-book.svgaddress-card.svgadjust.svgalign-center.svgalign-justify.svgalign-left.svgalign-right.svgallergies.svgambulance.svgamerican-sign-language-interpreting.svganchor.svgangle-double-down.svgangle-double-left.svgangle-double-right.svgangle-double-up.svgangle-down.svgangle-left.svgangle-right.svgangle-up.svgangry.svgarchive.svgarchway.svgarrow-alt-circle-down.svgarrow-alt-circle-left.svgarrow-alt-circle-right.svgarrow-alt-circle-up.svgarrow-circle-down.svgarrow-circle-left.svgarrow-circle-right.svgarrow-circle-up.svgarrow-down.svgarrow-left.svgarrow-right.svgarrow-up.svgarrows-alt-h.svgarrows-alt-v.svgarrows-alt.svgassistive-listening-systems.svgasterisk.svgat.svgatlas.svgaudio-description.svgaward.svgbackspace.svgbackward.svgbalance-scale.svgban.svgband-aid.svgbarcode.svgbars.svgbaseball-ball.svgbasketball-ball.svgbath.svgbattery-empty.svgbattery-full.svgbattery-half.svgbattery-quarter.svgbattery-three-quarters.svgbed.svgbeer.svgbell-slash.svgbell.svgbezier-curve.svgbicycle.svgbinoculars.svgbirthday-cake.svgblender.svgblind.svgbold.svgbolt.svgbomb.svgbong.svgbook-open.svgbook.svgbookmark.svgbowling-ball.svgbox-open.svgbox.svgboxes.svgbraille.svgbriefcase-medical.svgbriefcase.svgbroadcast-tower.svgbroom.svgbrush.svgbug.svgbuilding.svgbullhorn.svgbullseye.svgburn.svgbus-alt.svgbus.svgcalculator.svgcalendar-alt.svgcalendar-check.svgcalendar-minus.svgcalendar-plus.svgcalendar-times.svgcalendar.svgcamera-retro.svgcamera.svgcannabis.svgcapsules.svgcar.svgcaret-down.svgcaret-left.svgcaret-right.svgcaret-square-down.svgcaret-square-left.svgcaret-square-right.svgcaret-square-up.svgcaret-up.svgcart-arrow-down.svgcart-plus.svgcertificate.svgchalkboard-teacher.svgchalkboard.svgchart-area.svgchart-bar.svgchart-line.svgchart-pie.svgcheck-circle.svgcheck-double.svgcheck-square.svgcheck.svgchess-bishop.svgchess-board.svgchess-king.svgchess-knight.svgchess-pawn.svgchess-queen.svgchess-rook.svgchess.svgchevron-circle-down.svgchevron-circle-left.svgchevron-circle-right.svgchevron-circle-up.svgchevron-down.svgchevron-left.svgchevron-right.svgchevron-up.svgchild.svgchurch.svgcircle-notch.svgcircle.svgclipboard-check.svgclipboard-list.svgclipboard.svgclock.svgclone.svgclosed-captioning.svgcloud-download-alt.svgcloud-upload-alt.svgcloud.svgcocktail.svgcode-branch.svgcode.svgcoffee.svgcog.svgcogs.svgcoins.svgcolumns.svgcomment-alt.svgcomment-dots.svgcomment-slash.svgcomment.svgcomments.svgcompact-disc.svgcompass.svgcompress.svgconcierge-bell.svgcookie-bite.svgcookie.svgcopy.svgcopyright.svgcouch.svgcredit-card.svgcrop-alt.svgcrop.svgcrosshairs.svgcrow.svgcrown.svgcube.svgcubes.svgcut.svgdatabase.svgdeaf.svgdesktop.svgdiagnoses.svgdice-five.svgdice-four.svgdice-one.svgdice-six.svgdice-three.svgdice-two.svgdice.svgdigital-tachograph.svgdivide.svgdizzy.svgdna.svgdollar-sign.svgdolly-flatbed.svgdolly.svgdonate.svgdoor-closed.svgdoor-open.svgdot-circle.svgdove.svgdownload.svgdrafting-compass.svgdrum-steelpan.svgdrum.svgdumbbell.svgedit.svgeject.svgellipsis-h.svgellipsis-v.svgenvelope-open.svgenvelope-square.svgenvelope.svgequals.svgeraser.svgeuro-sign.svgexchange-alt.svgexclamation-circle.svgexclamation-triangle.svgexclamation.svgexpand-arrows-alt.svgexpand.svgexternal-link-alt.svgexternal-link-square-alt.svgeye-dropper.svgeye-slash.svgeye.svgfast-backward.svgfast-forward.svgfax.svgfeather-alt.svgfeather.svgfemale.svgfighter-jet.svgfile-alt.svgfile-archive.svgfile-audio.svgfile-code.svgfile-contract.svgfile-download.svgfile-excel.svgfile-export.svgfile-image.svgfile-import.svgfile-invoice-dollar.svgfile-invoice.svgfile-medical-alt.svgfile-medical.svgfile-pdf.svgfile-powerpoint.svgfile-prescription.svgfile-signature.svgfile-upload.svgfile-video.svgfile-word.svgfile.svgfill-drip.svgfill.svgfilm.svgfilter.svgfingerprint.svgfire-extinguisher.svgfire.svgfirst-aid.svgfish.svgflag-checkered.svgflag.svgflask.svgflushed.svgfolder-open.svgfolder.svgfont-awesome-logo-full.svgfont.svgfootball-ball.svgforward.svgfrog.svgfrown-open.svgfrown.svgfutbol.svggamepad.svggas-pump.svggavel.svggem.svggenderless.svggift.svgglass-martini-alt.svgglass-martini.svgglasses.svgglobe-africa.svgglobe-americas.svgglobe-asia.svgglobe.svggolf-ball.svggraduation-cap.svggreater-than-equal.svggreater-than.svggrimace.svggrin-alt.svggrin-beam-sweat.svggrin-beam.svggrin-hearts.svggrin-squint-tears.svggrin-squint.svggrin-stars.svggrin-tears.svggrin-tongue-squint.svggrin-tongue-wink.svggrin-tongue.svggrin-wink.svggrin.svggrip-horizontal.svggrip-vertical.svgh-square.svghand-holding-heart.svghand-holding-usd.svghand-holding.svghand-lizard.svghand-paper.svghand-peace.svghand-point-down.svghand-point-left.svghand-point-right.svghand-point-up.svghand-pointer.svghand-rock.svghand-scissors.svghand-spock.svghands-helping.svghands.svghandshake.svghashtag.svghdd.svgheading.svgheadphones-alt.svgheadphones.svgheadset.svgheart.svgheartbeat.svghelicopter.svghighlighter.svghistory.svghockey-puck.svghome.svghospital-alt.svghospital-symbol.svghospital.svghot-tub.svghotel.svghourglass-end.svghourglass-half.svghourglass-start.svghourglass.svgi-cursor.svgid-badge.svgid-card-alt.svgid-card.svgimage.svgimages.svginbox.svgindent.svgindustry.svginfinity.svginfo-circle.svginfo.svgitalic.svgjoint.svgkey.svgkeyboard.svgkiss-beam.svgkiss-wink-heart.svgkiss.svgkiwi-bird.svglanguage.svglaptop.svglaugh-beam.svglaugh-squint.svglaugh-wink.svglaugh.svgleaf.svglemon.svgless-than-equal.svgless-than.svglevel-down-alt.svglevel-up-alt.svglife-ring.svglightbulb.svglink.svglira-sign.svglist-alt.svglist-ol.svglist-ul.svglist.svglocation-arrow.svglock-open.svglock.svglong-arrow-alt-down.svglong-arrow-alt-left.svglong-arrow-alt-right.svglong-arrow-alt-up.svglow-vision.svgluggage-cart.svgmagic.svgmagnet.svgmale.svgmap-marked-alt.svgmap-marked.svgmap-marker-alt.svgmap-marker.svgmap-pin.svgmap-signs.svgmap.svgmarker.svgmars-double.svgmars-stroke-h.svgmars-stroke-v.svgmars-stroke.svgmars.svgmedal.svgmedkit.svgmeh-blank.svgmeh-rolling-eyes.svgmeh.svgmemory.svgmercury.svgmicrochip.svgmicrophone-alt-slash.svgmicrophone-alt.svgmicrophone-slash.svgmicrophone.svgminus-circle.svgminus-square.svgminus.svgmobile-alt.svgmobile.svgmoney-bill-alt.svgmoney-bill-wave-alt.svgmoney-bill-wave.svgmoney-bill.svgmoney-check-alt.svgmoney-check.svgmonument.svgmoon.svgmortar-pestle.svgmotorcycle.svgmouse-pointer.svgmusic.svgneuter.svgnewspaper.svgnot-equal.svgnotes-medical.svgobject-group.svgobject-ungroup.svgoutdent.svgpaint-brush.svgpaint-roller.svgpalette.svgpallet.svgpaper-plane.svgpaperclip.svgparachute-box.svgparagraph.svgparking.svgpassport.svgpaste.svgpause-circle.svgpause.svgpaw.svgpen-alt.svgpen-fancy.svgpen-nib.svgpen-square.svgpen.svgpencil-alt.svgpencil-ruler.svgpeople-carry.svgpercent.svgpercentage.svgphone-slash.svgphone-square.svgphone-volume.svgphone.svgpiggy-bank.svgpills.svgplane-arrival.svgplane-departure.svgplane.svgplay-circle.svgplay.svgplug.svgplus-circle.svgplus-square.svgplus.svgpodcast.svgpoo.svgportrait.svgpound-sign.svgpower-off.svgprescription-bottle-alt.svgprescription-bottle.svgprescription.svgprint.svgprocedures.svgproject-diagram.svgpuzzle-piece.svgqrcode.svgquestion-circle.svgquestion.svgquidditch.svgquote-left.svgquote-right.svgrandom.svgreceipt.svgrecycle.svgredo-alt.svgredo.svgregistered.svgreply-all.svgreply.svgretweet.svgribbon.svgroad.svgrobot.svgrocket.svgrss-square.svgrss.svgruble-sign.svgruler-combined.svgruler-horizontal.svgruler-vertical.svgruler.svgrupee-sign.svgsad-cry.svgsad-tear.svgsave.svgschool.svgscrewdriver.svgsearch-minus.svgsearch-plus.svgsearch.svgseedling.svgserver.svgshare-alt-square.svgshare-alt.svgshare-square.svgshare.svgshekel-sign.svgshield-alt.svgship.svgshipping-fast.svgshoe-prints.svgshopping-bag.svgshopping-basket.svgshopping-cart.svgshower.svgshuttle-van.svgsign-in-alt.svgsign-language.svgsign-out-alt.svgsign.svgsignal.svgsignature.svgsitemap.svgskull.svgsliders-h.svgsmile-beam.svgsmile-wink.svgsmile.svgsmoking-ban.svgsmoking.svgsnowflake.svgsolar-panel.svgsort-alpha-down.svgsort-alpha-up.svgsort-amount-down.svgsort-amount-up.svgsort-down.svgsort-numeric-down.svgsort-numeric-up.svgsort-up.svgsort.svgspa.svgspace-shuttle.svgspinner.svgsplotch.svgspray-can.svgsquare-full.svgsquare.svgstamp.svgstar-half-alt.svgstar-half.svgstar.svgstep-backward.svgstep-forward.svgstethoscope.svgsticky-note.svgstop-circle.svgstop.svgstopwatch.svgstore-alt.svgstore.svgstream.svgstreet-view.svgstrikethrough.svgstroopwafel.svgsubscript.svgsubway.svgsuitcase-rolling.svgsuitcase.svgsun.svgsuperscript.svgsurprise.svgswatchbook.svgswimmer.svgswimming-pool.svgsync-alt.svgsync.svgsyringe.svgtable-tennis.svgtable.svgtablet-alt.svgtablet.svgtablets.svgtachometer-alt.svgtag.svgtags.svgtape.svgtasks.svgtaxi.svgterminal.svgtext-height.svgtext-width.svgth-large.svgth-list.svgth.svgthermometer-empty.svgthermometer-full.svgthermometer-half.svgthermometer-quarter.svgthermometer-three-quarters.svgthermometer.svgthumbs-down.svgthumbs-up.svgthumbtack.svgticket-alt.svgtimes-circle.svgtimes.svgtint-slash.svgtint.svgtired.svgtoggle-off.svgtoggle-on.svgtoolbox.svgtooth.svgtrademark.svgtrain.svgtransgender-alt.svgtransgender.svgtrash-alt.svgtrash.svgtree.svgtrophy.svgtruck-loading.svgtruck-moving.svgtruck.svgtshirt.svgtty.svgtv.svgumbrella-beach.svgumbrella.svgunderline.svgundo-alt.svgundo.svguniversal-access.svguniversity.svgunlink.svgunlock-alt.svgunlock.svgupload.svguser-alt-slash.svguser-alt.svguser-astronaut.svguser-check.svguser-circle.svguser-clock.svguser-cog.svguser-edit.svguser-friends.svguser-graduate.svguser-lock.svguser-md.svguser-minus.svguser-ninja.svguser-plus.svguser-secret.svguser-shield.svguser-slash.svguser-tag.svguser-tie.svguser-times.svguser.svgusers-cog.svgusers.svgutensil-spoon.svgutensils.svgvector-square.svgvenus-double.svgvenus-mars.svgvenus.svgvial.svgvials.svgvideo-slash.svgvideo.svgvolleyball-ball.svgvolume-down.svgvolume-off.svgvolume-up.svgwalking.svgwallet.svgwarehouse.svgweight-hanging.svgweight.svgwheelchair.svgwifi.svgwindow-close.svgwindow-maximize.svgwindow-minimize.svgwindow-restore.svgwine-glass-alt.svgwine-glass.svgwon-sign.svgwrench.svgx-ray.svgyen-sign.svg
svg-sprites
use-with-node-js
fontawesome-common-types
fontawesome-free-brands
LICENSE.txtREADME.mdfa500px.d.tsfa500px.jsfaAccessibleIcon.d.tsfaAccessibleIcon.jsfaAccusoft.d.tsfaAccusoft.jsfaAdn.d.tsfaAdn.jsfaAdversal.d.tsfaAdversal.jsfaAffiliatetheme.d.tsfaAffiliatetheme.jsfaAlgolia.d.tsfaAlgolia.jsfaAmazon.d.tsfaAmazon.jsfaAmazonPay.d.tsfaAmazonPay.jsfaAmilia.d.tsfaAmilia.jsfaAndroid.d.tsfaAndroid.jsfaAngellist.d.tsfaAngellist.jsfaAngrycreative.d.tsfaAngrycreative.jsfaAngular.d.tsfaAngular.jsfaAppStore.d.tsfaAppStore.jsfaAppStoreIos.d.tsfaAppStoreIos.jsfaApper.d.tsfaApper.jsfaApple.d.tsfaApple.jsfaApplePay.d.tsfaApplePay.jsfaAsymmetrik.d.tsfaAsymmetrik.jsfaAudible.d.tsfaAudible.jsfaAutoprefixer.d.tsfaAutoprefixer.jsfaAvianex.d.tsfaAvianex.jsfaAviato.d.tsfaAviato.jsfaAws.d.tsfaAws.jsfaBandcamp.d.tsfaBandcamp.jsfaBehance.d.tsfaBehance.jsfaBehanceSquare.d.tsfaBehanceSquare.jsfaBimobject.d.tsfaBimobject.jsfaBitbucket.d.tsfaBitbucket.jsfaBitcoin.d.tsfaBitcoin.jsfaBity.d.tsfaBity.jsfaBlackTie.d.tsfaBlackTie.jsfaBlackberry.d.tsfaBlackberry.jsfaBlogger.d.tsfaBlogger.jsfaBloggerB.d.tsfaBloggerB.jsfaBluetooth.d.tsfaBluetooth.jsfaBluetoothB.d.tsfaBluetoothB.jsfaBtc.d.tsfaBtc.jsfaBuromobelexperte.d.tsfaBuromobelexperte.jsfaBuysellads.d.tsfaBuysellads.jsfaCcAmazonPay.d.tsfaCcAmazonPay.jsfaCcAmex.d.tsfaCcAmex.jsfaCcApplePay.d.tsfaCcApplePay.jsfaCcDinersClub.d.tsfaCcDinersClub.jsfaCcDiscover.d.tsfaCcDiscover.jsfaCcJcb.d.tsfaCcJcb.jsfaCcMastercard.d.tsfaCcMastercard.jsfaCcPaypal.d.tsfaCcPaypal.jsfaCcStripe.d.tsfaCcStripe.jsfaCcVisa.d.tsfaCcVisa.jsfaCentercode.d.tsfaCentercode.jsfaChrome.d.tsfaChrome.jsfaCloudscale.d.tsfaCloudscale.jsfaCloudsmith.d.tsfaCloudsmith.jsfaCloudversify.d.tsfaCloudversify.jsfaCodepen.d.tsfaCodepen.jsfaCodiepie.d.tsfaCodiepie.jsfaConnectdevelop.d.tsfaConnectdevelop.jsfaContao.d.tsfaContao.jsfaCpanel.d.tsfaCpanel.jsfaCreativeCommons.d.tsfaCreativeCommons.jsfaCreativeCommonsBy.d.tsfaCreativeCommonsBy.jsfaCreativeCommonsNc.d.tsfaCreativeCommonsNc.jsfaCreativeCommonsNcEu.d.tsfaCreativeCommonsNcEu.jsfaCreativeCommonsNcJp.d.tsfaCreativeCommonsNcJp.jsfaCreativeCommonsNd.d.tsfaCreativeCommonsNd.jsfaCreativeCommonsPd.d.tsfaCreativeCommonsPd.jsfaCreativeCommonsPdAlt.d.tsfaCreativeCommonsPdAlt.jsfaCreativeCommonsRemix.d.tsfaCreativeCommonsRemix.jsfaCreativeCommonsSa.d.tsfaCreativeCommonsSa.jsfaCreativeCommonsSampling.d.tsfaCreativeCommonsSampling.jsfaCreativeCommonsSamplingPlus.d.tsfaCreativeCommonsSamplingPlus.jsfaCreativeCommonsShare.d.tsfaCreativeCommonsShare.jsfaCss3.d.tsfaCss3.jsfaCss3Alt.d.tsfaCss3Alt.jsfaCuttlefish.d.tsfaCuttlefish.jsfaDAndD.d.tsfaDAndD.jsfaDashcube.d.tsfaDashcube.jsfaDelicious.d.tsfaDelicious.jsfaDeploydog.d.tsfaDeploydog.jsfaDeskpro.d.tsfaDeskpro.jsfaDeviantart.d.tsfaDeviantart.jsfaDigg.d.tsfaDigg.jsfaDigitalOcean.d.tsfaDigitalOcean.jsfaDiscord.d.tsfaDiscord.jsfaDiscourse.d.tsfaDiscourse.jsfaDochub.d.tsfaDochub.jsfaDocker.d.tsfaDocker.jsfaDraft2digital.d.tsfaDraft2digital.jsfaDribbble.d.tsfaDribbble.jsfaDribbbleSquare.d.tsfaDribbbleSquare.jsfaDropbox.d.tsfaDropbox.jsfaDrupal.d.tsfaDrupal.jsfaDyalog.d.tsfaDyalog.jsfaEarlybirds.d.tsfaEarlybirds.jsfaEbay.d.tsfaEbay.jsfaEdge.d.tsfaEdge.jsfaElementor.d.tsfaElementor.jsfaEmber.d.tsfaEmber.jsfaEmpire.d.tsfaEmpire.jsfaEnvira.d.tsfaEnvira.jsfaErlang.d.tsfaErlang.jsfaEthereum.d.tsfaEthereum.jsfaEtsy.d.tsfaEtsy.jsfaExpeditedssl.d.tsfaExpeditedssl.jsfaFacebook.d.tsfaFacebook.jsfaFacebookF.d.tsfaFacebookF.jsfaFacebookMessenger.d.tsfaFacebookMessenger.jsfaFacebookSquare.d.tsfaFacebookSquare.jsfaFirefox.d.tsfaFirefox.jsfaFirstOrder.d.tsfaFirstOrder.jsfaFirstOrderAlt.d.tsfaFirstOrderAlt.jsfaFirstdraft.d.tsfaFirstdraft.jsfaFlickr.d.tsfaFlickr.jsfaFlipboard.d.tsfaFlipboard.jsfaFly.d.tsfaFly.jsfaFontAwesome.d.tsfaFontAwesome.jsfaFontAwesomeAlt.d.tsfaFontAwesomeAlt.jsfaFontAwesomeFlag.d.tsfaFontAwesomeFlag.jsfaFontAwesomeLogoFull.d.tsfaFontAwesomeLogoFull.jsfaFonticons.d.tsfaFonticons.jsfaFonticonsFi.d.tsfaFonticonsFi.jsfaFortAwesome.d.tsfaFortAwesome.jsfaFortAwesomeAlt.d.tsfaFortAwesomeAlt.jsfaForumbee.d.tsfaForumbee.jsfaFoursquare.d.tsfaFoursquare.jsfaFreeCodeCamp.d.tsfaFreeCodeCamp.jsfaFreebsd.d.tsfaFreebsd.jsfaFulcrum.d.tsfaFulcrum.jsfaGalacticRepublic.d.tsfaGalacticRepublic.jsfaGalacticSenate.d.tsfaGalacticSenate.jsfaGetPocket.d.tsfaGetPocket.jsfaGg.d.tsfaGg.jsfaGgCircle.d.tsfaGgCircle.jsfaGit.d.tsfaGit.jsfaGitSquare.d.tsfaGitSquare.jsfaGithub.d.tsfaGithub.jsfaGithubAlt.d.tsfaGithubAlt.jsfaGithubSquare.d.tsfaGithubSquare.jsfaGitkraken.d.tsfaGitkraken.jsfaGitlab.d.tsfaGitlab.jsfaGitter.d.tsfaGitter.jsfaGlide.d.tsfaGlide.jsfaGlideG.d.tsfaGlideG.jsfaGofore.d.tsfaGofore.jsfaGoodreads.d.tsfaGoodreads.jsfaGoodreadsG.d.tsfaGoodreadsG.jsfaGoogle.d.tsfaGoogle.jsfaGoogleDrive.d.tsfaGoogleDrive.jsfaGooglePlay.d.tsfaGooglePlay.jsfaGooglePlus.d.tsfaGooglePlus.jsfaGooglePlusG.d.tsfaGooglePlusG.jsfaGooglePlusSquare.d.tsfaGooglePlusSquare.jsfaGoogleWallet.d.tsfaGoogleWallet.jsfaGratipay.d.tsfaGratipay.jsfaGrav.d.tsfaGrav.jsfaGripfire.d.tsfaGripfire.jsfaGrunt.d.tsfaGrunt.jsfaGulp.d.tsfaGulp.jsfaHackerNews.d.tsfaHackerNews.jsfaHackerNewsSquare.d.tsfaHackerNewsSquare.jsfaHips.d.tsfaHips.jsfaHireAHelper.d.tsfaHireAHelper.jsfaHooli.d.tsfaHooli.jsfaHotjar.d.tsfaHotjar.jsfaHouzz.d.tsfaHouzz.jsfaHtml5.d.tsfaHtml5.jsfaHubspot.d.tsfaHubspot.jsfaImdb.d.tsfaImdb.jsfaInstagram.d.tsfaInstagram.jsfaInternetExplorer.d.tsfaInternetExplorer.jsfaIoxhost.d.tsfaIoxhost.jsfaItunes.d.tsfaItunes.jsfaItunesNote.d.tsfaItunesNote.jsfaJava.d.tsfaJava.jsfaJediOrder.d.tsfaJediOrder.jsfaJenkins.d.tsfaJenkins.jsfaJoget.d.tsfaJoget.jsfaJoomla.d.tsfaJoomla.jsfaJs.d.tsfaJs.jsfaJsSquare.d.tsfaJsSquare.jsfaJsfiddle.d.tsfaJsfiddle.jsfaKeybase.d.tsfaKeybase.jsfaKeycdn.d.tsfaKeycdn.jsfaKickstarter.d.tsfaKickstarter.jsfaKickstarterK.d.tsfaKickstarterK.jsfaKorvue.d.tsfaKorvue.jsfaLaravel.d.tsfaLaravel.jsfaLastfm.d.tsfaLastfm.jsfaLastfmSquare.d.tsfaLastfmSquare.jsfaLeanpub.d.tsfaLeanpub.jsfaLess.d.tsfaLess.jsfaLine.d.tsfaLine.jsfaLinkedin.d.tsfaLinkedin.jsfaLinkedinIn.d.tsfaLinkedinIn.jsfaLinode.d.tsfaLinode.jsfaLinux.d.tsfaLinux.jsfaLyft.d.tsfaLyft.jsfaMagento.d.tsfaMagento.jsfaMandalorian.d.tsfaMandalorian.jsfaMastodon.d.tsfaMastodon.jsfaMaxcdn.d.tsfaMaxcdn.jsfaMedapps.d.tsfaMedapps.jsfaMedium.d.tsfaMedium.jsfaMediumM.d.tsfaMediumM.jsfaMedrt.d.tsfaMedrt.jsfaMeetup.d.tsfaMeetup.jsfaMicrosoft.d.tsfaMicrosoft.jsfaMix.d.tsfaMix.jsfaMixcloud.d.tsfaMixcloud.jsfaMizuni.d.tsfaMizuni.jsfaModx.d.tsfaModx.jsfaMonero.d.tsfaMonero.jsfaNapster.d.tsfaNapster.jsfaNintendoSwitch.d.tsfaNintendoSwitch.jsfaNode.d.tsfaNode.jsfaNodeJs.d.tsfaNodeJs.jsfaNpm.d.tsfaNpm.jsfaNs8.d.tsfaNs8.jsfaNutritionix.d.tsfaNutritionix.jsfaOdnoklassniki.d.tsfaOdnoklassniki.jsfaOdnoklassnikiSquare.d.tsfaOdnoklassnikiSquare.jsfaOldRepublic.d.tsfaOldRepublic.jsfaOpencart.d.tsfaOpencart.jsfaOpenid.d.tsfaOpenid.jsfaOpera.d.tsfaOpera.jsfaOptinMonster.d.tsfaOptinMonster.jsfaOsi.d.tsfaOsi.jsfaPage4.d.tsfaPage4.jsfaPagelines.d.tsfaPagelines.jsfaPalfed.d.tsfaPalfed.jsfaPatreon.d.tsfaPatreon.jsfaPaypal.d.tsfaPaypal.jsfaPeriscope.d.tsfaPeriscope.jsfaPhabricator.d.tsfaPhabricator.jsfaPhoenixFramework.d.tsfaPhoenixFramework.jsfaPhoenixSquadron.d.tsfaPhoenixSquadron.jsfaPhp.d.tsfaPhp.jsfaPiedPiper.d.tsfaPiedPiper.jsfaPiedPiperAlt.d.tsfaPiedPiperAlt.jsfaPiedPiperHat.d.tsfaPiedPiperHat.jsfaPiedPiperPp.d.tsfaPiedPiperPp.jsfaPinterest.d.tsfaPinterest.jsfaPinterestP.d.tsfaPinterestP.jsfaPinterestSquare.d.tsfaPinterestSquare.jsfaPlaystation.d.tsfaPlaystation.jsfaProductHunt.d.tsfaProductHunt.jsfaPushed.d.tsfaPushed.jsfaPython.d.tsfaPython.jsfaQq.d.tsfaQq.jsfaQuinscape.d.tsfaQuinscape.jsfaQuora.d.tsfaQuora.jsfaR.d.tsfaR.jsfaRProject.d.tsfaRProject.jsfaRavelry.d.tsfaRavelry.jsfaReact.d.tsfaReact.jsfaReadme.d.tsfaReadme.jsfaRebel.d.tsfaRebel.jsfaRedRiver.d.tsfaRedRiver.jsfaReddit.d.tsfaReddit.jsfaRedditAlien.d.tsfaRedditAlien.jsfaRedditSquare.d.tsfaRedditSquare.jsfaRendact.d.tsfaRendact.jsfaRenren.d.tsfaRenren.jsfaReplyd.d.tsfaReplyd.jsfaResearchgate.d.tsfaResearchgate.jsfaResolving.d.tsfaResolving.jsfaRocketchat.d.tsfaRocketchat.jsfaRockrms.d.tsfaRockrms.jsfaSafari.d.tsfaSafari.jsfaSass.d.tsfaSass.jsfaSchlix.d.tsfaSchlix.jsfaScribd.d.tsfaScribd.jsfaSearchengin.d.tsfaSearchengin.jsfaSellcast.d.tsfaSellcast.jsfaSellsy.d.tsfaSellsy.jsfaServicestack.d.tsfaServicestack.jsfaShirtsinbulk.d.tsfaShirtsinbulk.jsfaSimplybuilt.d.tsfaSimplybuilt.jsfaSistrix.d.tsfaSistrix.jsfaSith.d.tsfaSith.jsfaSkyatlas.d.tsfaSkyatlas.jsfaSkype.d.tsfaSkype.jsfaSlack.d.tsfaSlack.jsfaSlackHash.d.tsfaSlackHash.jsfaSlideshare.d.tsfaSlideshare.jsfaSnapchat.d.tsfaSnapchat.jsfaSnapchatGhost.d.tsfaSnapchatGhost.jsfaSnapchatSquare.d.tsfaSnapchatSquare.jsfaSoundcloud.d.tsfaSoundcloud.jsfaSpeakap.d.tsfaSpeakap.jsfaSpotify.d.tsfaSpotify.jsfaStackExchange.d.tsfaStackExchange.jsfaStackOverflow.d.tsfaStackOverflow.jsfaStaylinked.d.tsfaStaylinked.jsfaSteam.d.tsfaSteam.jsfaSteamSquare.d.tsfaSteamSquare.jsfaSteamSymbol.d.tsfaSteamSymbol.jsfaStickerMule.d.tsfaStickerMule.jsfaStrava.d.tsfaStrava.jsfaStripe.d.tsfaStripe.jsfaStripeS.d.tsfaStripeS.jsfaStudiovinari.d.tsfaStudiovinari.jsfaStumbleupon.d.tsfaStumbleupon.jsfaStumbleuponCircle.d.tsfaStumbleuponCircle.jsfaSuperpowers.d.tsfaSuperpowers.jsfaSupple.d.tsfaSupple.jsfaTeamspeak.d.tsfaTeamspeak.jsfaTelegram.d.tsfaTelegram.jsfaTelegramPlane.d.tsfaTelegramPlane.jsfaTencentWeibo.d.tsfaTencentWeibo.jsfaThemeisle.d.tsfaThemeisle.jsfaTradeFederation.d.tsfaTradeFederation.jsfaTrello.d.tsfaTrello.jsfaTripadvisor.d.tsfaTripadvisor.jsfaTumblr.d.tsfaTumblr.jsfaTumblrSquare.d.tsfaTumblrSquare.jsfaTwitch.d.tsfaTwitch.jsfaTwitter.d.tsfaTwitter.jsfaTwitterSquare.d.tsfaTwitterSquare.jsfaTypo3.d.tsfaTypo3.jsfaUber.d.tsfaUber.jsfaUikit.d.tsfaUikit.jsfaUniregistry.d.tsfaUniregistry.jsfaUntappd.d.tsfaUntappd.jsfaUsb.d.tsfaUsb.jsfaUssunnah.d.tsfaUssunnah.jsfaVaadin.d.tsfaVaadin.jsfaViacoin.d.tsfaViacoin.jsfaViadeo.d.tsfaViadeo.jsfaViadeoSquare.d.tsfaViadeoSquare.jsfaViber.d.tsfaViber.jsfaVimeo.d.tsfaVimeo.jsfaVimeoSquare.d.tsfaVimeoSquare.jsfaVimeoV.d.tsfaVimeoV.jsfaVine.d.tsfaVine.jsfaVk.d.tsfaVk.jsfaVnv.d.tsfaVnv.jsfaVuejs.d.tsfaVuejs.jsfaWeibo.d.tsfaWeibo.jsfaWeixin.d.tsfaWeixin.jsfaWhatsapp.d.tsfaWhatsapp.jsfaWhatsappSquare.d.tsfaWhatsappSquare.jsfaWhmcs.d.tsfaWhmcs.jsfaWikipediaW.d.tsfaWikipediaW.jsfaWindows.d.tsfaWindows.jsfaWolfPackBattalion.d.tsfaWolfPackBattalion.jsfaWordpress.d.tsfaWordpress.jsfaWordpressSimple.d.tsfaWordpressSimple.jsfaWpbeginner.d.tsfaWpbeginner.jsfaWpexplorer.d.tsfaWpexplorer.jsfaWpforms.d.tsfaWpforms.jsfaXbox.d.tsfaXbox.jsfaXing.d.tsfaXing.jsfaXingSquare.d.tsfaXingSquare.jsfaYCombinator.d.tsfaYCombinator.jsfaYahoo.d.tsfaYahoo.jsfaYandex.d.tsfaYandex.jsfaYandexInternational.d.tsfaYandexInternational.jsfaYelp.d.tsfaYelp.jsfaYoast.d.tsfaYoast.jsfaYoutube.d.tsfaYoutube.jsfaYoutubeSquare.d.tsfaYoutubeSquare.jsindex.d.tsindex.es.jsindex.jspackage.jsonshakable.es.jsshakable.js
fontawesome-free-regular
LICENSE.txtREADME.mdfaAddressBook.d.tsfaAddressBook.jsfaAddressCard.d.tsfaAddressCard.jsfaArrowAltCircleDown.d.tsfaArrowAltCircleDown.jsfaArrowAltCircleLeft.d.tsfaArrowAltCircleLeft.jsfaArrowAltCircleRight.d.tsfaArrowAltCircleRight.jsfaArrowAltCircleUp.d.tsfaArrowAltCircleUp.jsfaBell.d.tsfaBell.jsfaBellSlash.d.tsfaBellSlash.jsfaBookmark.d.tsfaBookmark.jsfaBuilding.d.tsfaBuilding.jsfaCalendar.d.tsfaCalendar.jsfaCalendarAlt.d.tsfaCalendarAlt.jsfaCalendarCheck.d.tsfaCalendarCheck.jsfaCalendarMinus.d.tsfaCalendarMinus.jsfaCalendarPlus.d.tsfaCalendarPlus.jsfaCalendarTimes.d.tsfaCalendarTimes.jsfaCaretSquareDown.d.tsfaCaretSquareDown.jsfaCaretSquareLeft.d.tsfaCaretSquareLeft.jsfaCaretSquareRight.d.tsfaCaretSquareRight.jsfaCaretSquareUp.d.tsfaCaretSquareUp.jsfaChartBar.d.tsfaChartBar.jsfaCheckCircle.d.tsfaCheckCircle.jsfaCheckSquare.d.tsfaCheckSquare.jsfaCircle.d.tsfaCircle.jsfaClipboard.d.tsfaClipboard.jsfaClock.d.tsfaClock.jsfaClone.d.tsfaClone.jsfaClosedCaptioning.d.tsfaClosedCaptioning.jsfaComment.d.tsfaComment.jsfaCommentAlt.d.tsfaCommentAlt.jsfaCommentDots.d.tsfaCommentDots.jsfaComments.d.tsfaComments.jsfaCompass.d.tsfaCompass.jsfaCopy.d.tsfaCopy.jsfaCopyright.d.tsfaCopyright.jsfaCreditCard.d.tsfaCreditCard.jsfaDotCircle.d.tsfaDotCircle.jsfaEdit.d.tsfaEdit.jsfaEnvelope.d.tsfaEnvelope.jsfaEnvelopeOpen.d.tsfaEnvelopeOpen.jsfaEye.d.tsfaEye.jsfaEyeSlash.d.tsfaEyeSlash.jsfaFile.d.tsfaFile.jsfaFileAlt.d.tsfaFileAlt.jsfaFileArchive.d.tsfaFileArchive.jsfaFileAudio.d.tsfaFileAudio.jsfaFileCode.d.tsfaFileCode.jsfaFileExcel.d.tsfaFileExcel.jsfaFileImage.d.tsfaFileImage.jsfaFilePdf.d.tsfaFilePdf.jsfaFilePowerpoint.d.tsfaFilePowerpoint.jsfaFileVideo.d.tsfaFileVideo.jsfaFileWord.d.tsfaFileWord.jsfaFlag.d.tsfaFlag.jsfaFolder.d.tsfaFolder.jsfaFolderOpen.d.tsfaFolderOpen.jsfaFontAwesomeLogoFull.d.tsfaFontAwesomeLogoFull.jsfaFrown.d.tsfaFrown.jsfaFutbol.d.tsfaFutbol.jsfaGem.d.tsfaGem.jsfaHandLizard.d.tsfaHandLizard.jsfaHandPaper.d.tsfaHandPaper.jsfaHandPeace.d.tsfaHandPeace.jsfaHandPointDown.d.tsfaHandPointDown.jsfaHandPointLeft.d.tsfaHandPointLeft.jsfaHandPointRight.d.tsfaHandPointRight.jsfaHandPointUp.d.tsfaHandPointUp.jsfaHandPointer.d.tsfaHandPointer.jsfaHandRock.d.tsfaHandRock.jsfaHandScissors.d.tsfaHandScissors.jsfaHandSpock.d.tsfaHandSpock.jsfaHandshake.d.tsfaHandshake.jsfaHdd.d.tsfaHdd.jsfaHeart.d.tsfaHeart.jsfaHospital.d.tsfaHospital.jsfaHourglass.d.tsfaHourglass.jsfaIdBadge.d.tsfaIdBadge.jsfaIdCard.d.tsfaIdCard.jsfaImage.d.tsfaImage.jsfaImages.d.tsfaImages.jsfaKeyboard.d.tsfaKeyboard.jsfaLemon.d.tsfaLemon.jsfaLifeRing.d.tsfaLifeRing.jsfaLightbulb.d.tsfaLightbulb.jsfaListAlt.d.tsfaListAlt.jsfaMap.d.tsfaMap.jsfaMeh.d.tsfaMeh.jsfaMinusSquare.d.tsfaMinusSquare.jsfaMoneyBillAlt.d.tsfaMoneyBillAlt.jsfaMoon.d.tsfaMoon.jsfaNewspaper.d.tsfaNewspaper.jsfaObjectGroup.d.tsfaObjectGroup.jsfaObjectUngroup.d.tsfaObjectUngroup.jsfaPaperPlane.d.tsfaPaperPlane.jsfaPauseCircle.d.tsfaPauseCircle.jsfaPlayCircle.d.tsfaPlayCircle.jsfaPlusSquare.d.tsfaPlusSquare.jsfaQuestionCircle.d.tsfaQuestionCircle.jsfaRegistered.d.tsfaRegistered.jsfaSave.d.tsfaSave.jsfaShareSquare.d.tsfaShareSquare.jsfaSmile.d.tsfaSmile.jsfaSnowflake.d.tsfaSnowflake.jsfaSquare.d.tsfaSquare.jsfaStar.d.tsfaStar.jsfaStarHalf.d.tsfaStarHalf.jsfaStickyNote.d.tsfaStickyNote.jsfaStopCircle.d.tsfaStopCircle.jsfaSun.d.tsfaSun.jsfaThumbsDown.d.tsfaThumbsDown.jsfaThumbsUp.d.tsfaThumbsUp.jsfaTimesCircle.d.tsfaTimesCircle.jsfaTrashAlt.d.tsfaTrashAlt.jsfaUser.d.tsfaUser.jsfaUserCircle.d.tsfaUserCircle.jsfaWindowClose.d.tsfaWindowClose.jsfaWindowMaximize.d.tsfaWindowMaximize.jsfaWindowMinimize.d.tsfaWindowMinimize.jsfaWindowRestore.d.tsfaWindowRestore.jsindex.d.tsindex.es.jsindex.jspackage.jsonshakable.es.jsshakable.js
fontawesome-free-solid
LICENSE.txtREADME.mdfaAddressBook.d.tsfaAddressBook.jsfaAddressCard.d.tsfaAddressCard.jsfaAdjust.d.tsfaAdjust.jsfaAlignCenter.d.tsfaAlignCenter.jsfaAlignJustify.d.tsfaAlignJustify.jsfaAlignLeft.d.tsfaAlignLeft.jsfaAlignRight.d.tsfaAlignRight.jsfaAllergies.d.tsfaAllergies.jsfaAmbulance.d.tsfaAmbulance.jsfaAmericanSignLanguageInterpreting.d.tsfaAmericanSignLanguageInterpreting.jsfaAnchor.d.tsfaAnchor.jsfaAngleDoubleDown.d.tsfaAngleDoubleDown.jsfaAngleDoubleLeft.d.tsfaAngleDoubleLeft.jsfaAngleDoubleRight.d.tsfaAngleDoubleRight.jsfaAngleDoubleUp.d.tsfaAngleDoubleUp.jsfaAngleDown.d.tsfaAngleDown.jsfaAngleLeft.d.tsfaAngleLeft.jsfaAngleRight.d.tsfaAngleRight.jsfaAngleUp.d.tsfaAngleUp.jsfaArchive.d.tsfaArchive.jsfaArrowAltCircleDown.d.tsfaArrowAltCircleDown.jsfaArrowAltCircleLeft.d.tsfaArrowAltCircleLeft.jsfaArrowAltCircleRight.d.tsfaArrowAltCircleRight.jsfaArrowAltCircleUp.d.tsfaArrowAltCircleUp.jsfaArrowCircleDown.d.tsfaArrowCircleDown.jsfaArrowCircleLeft.d.tsfaArrowCircleLeft.jsfaArrowCircleRight.d.tsfaArrowCircleRight.jsfaArrowCircleUp.d.tsfaArrowCircleUp.jsfaArrowDown.d.tsfaArrowDown.jsfaArrowLeft.d.tsfaArrowLeft.jsfaArrowRight.d.tsfaArrowRight.jsfaArrowUp.d.tsfaArrowUp.jsfaArrowsAlt.d.tsfaArrowsAlt.jsfaArrowsAltH.d.tsfaArrowsAltH.jsfaArrowsAltV.d.tsfaArrowsAltV.jsfaAssistiveListeningSystems.d.tsfaAssistiveListeningSystems.jsfaAsterisk.d.tsfaAsterisk.jsfaAt.d.tsfaAt.jsfaAudioDescription.d.tsfaAudioDescription.jsfaBackward.d.tsfaBackward.jsfaBalanceScale.d.tsfaBalanceScale.jsfaBan.d.tsfaBan.jsfaBandAid.d.tsfaBandAid.jsfaBarcode.d.tsfaBarcode.jsfaBars.d.tsfaBars.jsfaBaseballBall.d.tsfaBaseballBall.jsfaBasketballBall.d.tsfaBasketballBall.jsfaBath.d.tsfaBath.jsfaBatteryEmpty.d.tsfaBatteryEmpty.jsfaBatteryFull.d.tsfaBatteryFull.jsfaBatteryHalf.d.tsfaBatteryHalf.jsfaBatteryQuarter.d.tsfaBatteryQuarter.jsfaBatteryThreeQuarters.d.tsfaBatteryThreeQuarters.jsfaBed.d.tsfaBed.jsfaBeer.d.tsfaBeer.jsfaBell.d.tsfaBell.jsfaBellSlash.d.tsfaBellSlash.jsfaBicycle.d.tsfaBicycle.jsfaBinoculars.d.tsfaBinoculars.jsfaBirthdayCake.d.tsfaBirthdayCake.jsfaBlender.d.tsfaBlender.jsfaBlind.d.tsfaBlind.jsfaBold.d.tsfaBold.jsfaBolt.d.tsfaBolt.jsfaBomb.d.tsfaBomb.jsfaBook.d.tsfaBook.jsfaBookOpen.d.tsfaBookOpen.jsfaBookmark.d.tsfaBookmark.jsfaBowlingBall.d.tsfaBowlingBall.jsfaBox.d.tsfaBox.jsfaBoxOpen.d.tsfaBoxOpen.jsfaBoxes.d.tsfaBoxes.jsfaBraille.d.tsfaBraille.jsfaBriefcase.d.tsfaBriefcase.jsfaBriefcaseMedical.d.tsfaBriefcaseMedical.jsfaBroadcastTower.d.tsfaBroadcastTower.jsfaBroom.d.tsfaBroom.jsfaBug.d.tsfaBug.jsfaBuilding.d.tsfaBuilding.jsfaBullhorn.d.tsfaBullhorn.jsfaBullseye.d.tsfaBullseye.jsfaBurn.d.tsfaBurn.jsfaBus.d.tsfaBus.jsfaCalculator.d.tsfaCalculator.jsfaCalendar.d.tsfaCalendar.jsfaCalendarAlt.d.tsfaCalendarAlt.jsfaCalendarCheck.d.tsfaCalendarCheck.jsfaCalendarMinus.d.tsfaCalendarMinus.jsfaCalendarPlus.d.tsfaCalendarPlus.jsfaCalendarTimes.d.tsfaCalendarTimes.jsfaCamera.d.tsfaCamera.jsfaCameraRetro.d.tsfaCameraRetro.jsfaCapsules.d.tsfaCapsules.jsfaCar.d.tsfaCar.jsfaCaretDown.d.tsfaCaretDown.jsfaCaretLeft.d.tsfaCaretLeft.jsfaCaretRight.d.tsfaCaretRight.jsfaCaretSquareDown.d.tsfaCaretSquareDown.jsfaCaretSquareLeft.d.tsfaCaretSquareLeft.jsfaCaretSquareRight.d.tsfaCaretSquareRight.jsfaCaretSquareUp.d.tsfaCaretSquareUp.jsfaCaretUp.d.tsfaCaretUp.jsfaCartArrowDown.d.tsfaCartArrowDown.jsfaCartPlus.d.tsfaCartPlus.jsfaCertificate.d.tsfaCertificate.jsfaChalkboard.d.tsfaChalkboard.jsfaChalkboardTeacher.d.tsfaChalkboardTeacher.jsfaChartArea.d.tsfaChartArea.jsfaChartBar.d.tsfaChartBar.jsfaChartLine.d.tsfaChartLine.jsfaChartPie.d.tsfaChartPie.jsfaCheck.d.tsfaCheck.jsfaCheckCircle.d.tsfaCheckCircle.jsfaCheckSquare.d.tsfaCheckSquare.jsfaChess.d.tsfaChess.jsfaChessBishop.d.tsfaChessBishop.jsfaChessBoard.d.tsfaChessBoard.jsfaChessKing.d.tsfaChessKing.jsfaChessKnight.d.tsfaChessKnight.jsfaChessPawn.d.tsfaChessPawn.jsfaChessQueen.d.tsfaChessQueen.jsfaChessRook.d.tsfaChessRook.jsfaChevronCircleDown.d.tsfaChevronCircleDown.jsfaChevronCircleLeft.d.tsfaChevronCircleLeft.jsfaChevronCircleRight.d.tsfaChevronCircleRight.jsfaChevronCircleUp.d.tsfaChevronCircleUp.jsfaChevronDown.d.tsfaChevronDown.jsfaChevronLeft.d.tsfaChevronLeft.jsfaChevronRight.d.tsfaChevronRight.jsfaChevronUp.d.tsfaChevronUp.jsfaChild.d.tsfaChild.jsfaChurch.d.tsfaChurch.jsfaCircle.d.tsfaCircle.jsfaCircleNotch.d.tsfaCircleNotch.jsfaClipboard.d.tsfaClipboard.jsfaClipboardCheck.d.tsfaClipboardCheck.jsfaClipboardList.d.tsfaClipboardList.jsfaClock.d.tsfaClock.jsfaClone.d.tsfaClone.jsfaClosedCaptioning.d.tsfaClosedCaptioning.jsfaCloud.d.tsfaCloud.jsfaCloudDownloadAlt.d.tsfaCloudDownloadAlt.jsfaCloudUploadAlt.d.tsfaCloudUploadAlt.jsfaCode.d.tsfaCode.jsfaCodeBranch.d.tsfaCodeBranch.jsfaCoffee.d.tsfaCoffee.jsfaCog.d.tsfaCog.jsfaCogs.d.tsfaCogs.jsfaCoins.d.tsfaCoins.jsfaColumns.d.tsfaColumns.jsfaComment.d.tsfaComment.jsfaCommentAlt.d.tsfaCommentAlt.jsfaCommentDots.d.tsfaCommentDots.jsfaCommentSlash.d.tsfaCommentSlash.jsfaComments.d.tsfaComments.jsfaCompactDisc.d.tsfaCompactDisc.jsfaCompass.d.tsfaCompass.jsfaCompress.d.tsfaCompress.jsfaCopy.d.tsfaCopy.jsfaCopyright.d.tsfaCopyright.jsfaCouch.d.tsfaCouch.jsfaCreditCard.d.tsfaCreditCard.jsfaCrop.d.tsfaCrop.jsfaCrosshairs.d.tsfaCrosshairs.jsfaCrow.d.tsfaCrow.jsfaCrown.d.tsfaCrown.jsfaCube.d.tsfaCube.jsfaCubes.d.tsfaCubes.jsfaCut.d.tsfaCut.jsfaDatabase.d.tsfaDatabase.jsfaDeaf.d.tsfaDeaf.jsfaDesktop.d.tsfaDesktop.jsfaDiagnoses.d.tsfaDiagnoses.jsfaDice.d.tsfaDice.jsfaDiceFive.d.tsfaDiceFive.jsfaDiceFour.d.tsfaDiceFour.jsfaDiceOne.d.tsfaDiceOne.jsfaDiceSix.d.tsfaDiceSix.jsfaDiceThree.d.tsfaDiceThree.jsfaDiceTwo.d.tsfaDiceTwo.jsfaDivide.d.tsfaDivide.jsfaDna.d.tsfaDna.jsfaDollarSign.d.tsfaDollarSign.jsfaDolly.d.tsfaDolly.jsfaDollyFlatbed.d.tsfaDollyFlatbed.jsfaDonate.d.tsfaDonate.jsfaDoorClosed.d.tsfaDoorClosed.jsfaDoorOpen.d.tsfaDoorOpen.jsfaDotCircle.d.tsfaDotCircle.jsfaDove.d.tsfaDove.jsfaDownload.d.tsfaDownload.jsfaDumbbell.d.tsfaDumbbell.jsfaEdit.d.tsfaEdit.jsfaEject.d.tsfaEject.jsfaEllipsisH.d.tsfaEllipsisH.jsfaEllipsisV.d.tsfaEllipsisV.jsfaEnvelope.d.tsfaEnvelope.jsfaEnvelopeOpen.d.tsfaEnvelopeOpen.jsfaEnvelopeSquare.d.tsfaEnvelopeSquare.jsfaEquals.d.tsfaEquals.jsfaEraser.d.tsfaEraser.jsfaEuroSign.d.tsfaEuroSign.jsfaExchangeAlt.d.tsfaExchangeAlt.jsfaExclamation.d.tsfaExclamation.jsfaExclamationCircle.d.tsfaExclamationCircle.jsfaExclamationTriangle.d.tsfaExclamationTriangle.jsfaExpand.d.tsfaExpand.jsfaExpandArrowsAlt.d.tsfaExpandArrowsAlt.jsfaExternalLinkAlt.d.tsfaExternalLinkAlt.jsfaExternalLinkSquareAlt.d.tsfaExternalLinkSquareAlt.jsfaEye.d.tsfaEye.jsfaEyeDropper.d.tsfaEyeDropper.jsfaEyeSlash.d.tsfaEyeSlash.jsfaFastBackward.d.tsfaFastBackward.jsfaFastForward.d.tsfaFastForward.jsfaFax.d.tsfaFax.jsfaFeather.d.tsfaFeather.jsfaFemale.d.tsfaFemale.jsfaFighterJet.d.tsfaFighterJet.jsfaFile.d.tsfaFile.jsfaFileAlt.d.tsfaFileAlt.jsfaFileArchive.d.tsfaFileArchive.jsfaFileAudio.d.tsfaFileAudio.jsfaFileCode.d.tsfaFileCode.jsfaFileExcel.d.tsfaFileExcel.jsfaFileImage.d.tsfaFileImage.jsfaFileMedical.d.tsfaFileMedical.jsfaFileMedicalAlt.d.tsfaFileMedicalAlt.jsfaFilePdf.d.tsfaFilePdf.jsfaFilePowerpoint.d.tsfaFilePowerpoint.jsfaFileVideo.d.tsfaFileVideo.jsfaFileWord.d.tsfaFileWord.jsfaFilm.d.tsfaFilm.jsfaFilter.d.tsfaFilter.jsfaFire.d.tsfaFire.jsfaFireExtinguisher.d.tsfaFireExtinguisher.jsfaFirstAid.d.tsfaFirstAid.jsfaFlag.d.tsfaFlag.jsfaFlagCheckered.d.tsfaFlagCheckered.jsfaFlask.d.tsfaFlask.jsfaFolder.d.tsfaFolder.jsfaFolderOpen.d.tsfaFolderOpen.jsfaFont.d.tsfaFont.jsfaFontAwesomeLogoFull.d.tsfaFontAwesomeLogoFull.jsfaFootballBall.d.tsfaFootballBall.jsfaForward.d.tsfaForward.jsfaFrog.d.tsfaFrog.jsfaFrown.d.tsfaFrown.jsfaFutbol.d.tsfaFutbol.jsfaGamepad.d.tsfaGamepad.jsfaGasPump.d.tsfaGasPump.jsfaGavel.d.tsfaGavel.jsfaGem.d.tsfaGem.jsfaGenderless.d.tsfaGenderless.jsfaGift.d.tsfaGift.jsfaGlassMartini.d.tsfaGlassMartini.jsfaGlasses.d.tsfaGlasses.jsfaGlobe.d.tsfaGlobe.jsfaGolfBall.d.tsfaGolfBall.jsfaGraduationCap.d.tsfaGraduationCap.jsfaGreaterThan.d.tsfaGreaterThan.jsfaGreaterThanEqual.d.tsfaGreaterThanEqual.jsfaHSquare.d.tsfaHSquare.jsfaHandHolding.d.tsfaHandHolding.jsfaHandHoldingHeart.d.tsfaHandHoldingHeart.jsfaHandHoldingUsd.d.tsfaHandHoldingUsd.jsfaHandLizard.d.tsfaHandLizard.jsfaHandPaper.d.tsfaHandPaper.jsfaHandPeace.d.tsfaHandPeace.jsfaHandPointDown.d.tsfaHandPointDown.jsfaHandPointLeft.d.tsfaHandPointLeft.jsfaHandPointRight.d.tsfaHandPointRight.jsfaHandPointUp.d.tsfaHandPointUp.jsfaHandPointer.d.tsfaHandPointer.jsfaHandRock.d.tsfaHandRock.jsfaHandScissors.d.tsfaHandScissors.jsfaHandSpock.d.tsfaHandSpock.jsfaHands.d.tsfaHands.jsfaHandsHelping.d.tsfaHandsHelping.jsfaHandshake.d.tsfaHandshake.jsfaHashtag.d.tsfaHashtag.jsfaHdd.d.tsfaHdd.jsfaHeading.d.tsfaHeading.jsfaHeadphones.d.tsfaHeadphones.jsfaHeart.d.tsfaHeart.jsfaHeartbeat.d.tsfaHeartbeat.jsfaHelicopter.d.tsfaHelicopter.jsfaHistory.d.tsfaHistory.jsfaHockeyPuck.d.tsfaHockeyPuck.jsfaHome.d.tsfaHome.jsfaHospital.d.tsfaHospital.jsfaHospitalAlt.d.tsfaHospitalAlt.jsfaHospitalSymbol.d.tsfaHospitalSymbol.jsfaHourglass.d.tsfaHourglass.jsfaHourglassEnd.d.tsfaHourglassEnd.jsfaHourglassHalf.d.tsfaHourglassHalf.jsfaHourglassStart.d.tsfaHourglassStart.jsfaICursor.d.tsfaICursor.jsfaIdBadge.d.tsfaIdBadge.jsfaIdCard.d.tsfaIdCard.jsfaIdCardAlt.d.tsfaIdCardAlt.jsfaImage.d.tsfaImage.jsfaImages.d.tsfaImages.jsfaInbox.d.tsfaInbox.jsfaIndent.d.tsfaIndent.jsfaIndustry.d.tsfaIndustry.jsfaInfinity.d.tsfaInfinity.jsfaInfo.d.tsfaInfo.jsfaInfoCircle.d.tsfaInfoCircle.jsfaItalic.d.tsfaItalic.jsfaKey.d.tsfaKey.jsfaKeyboard.d.tsfaKeyboard.jsfaKiwiBird.d.tsfaKiwiBird.jsfaLanguage.d.tsfaLanguage.jsfaLaptop.d.tsfaLaptop.jsfaLeaf.d.tsfaLeaf.jsfaLemon.d.tsfaLemon.jsfaLessThan.d.tsfaLessThan.jsfaLessThanEqual.d.tsfaLessThanEqual.jsfaLevelDownAlt.d.tsfaLevelDownAlt.jsfaLevelUpAlt.d.tsfaLevelUpAlt.jsfaLifeRing.d.tsfaLifeRing.jsfaLightbulb.d.tsfaLightbulb.jsfaLink.d.tsfaLink.jsfaLiraSign.d.tsfaLiraSign.jsfaList.d.tsfaList.jsfaListAlt.d.tsfaListAlt.jsfaListOl.d.tsfaListOl.jsfaListUl.d.tsfaListUl.jsfaLocationArrow.d.tsfaLocationArrow.jsfaLock.d.tsfaLock.jsfaLockOpen.d.tsfaLockOpen.jsfaLongArrowAltDown.d.tsfaLongArrowAltDown.jsfaLongArrowAltLeft.d.tsfaLongArrowAltLeft.jsfaLongArrowAltRight.d.tsfaLongArrowAltRight.jsfaLongArrowAltUp.d.tsfaLongArrowAltUp.jsfaLowVision.d.tsfaLowVision.jsfaMagic.d.tsfaMagic.jsfaMagnet.d.tsfaMagnet.jsfaMale.d.tsfaMale.jsfaMap.d.tsfaMap.jsfaMapMarker.d.tsfaMapMarker.jsfaMapMarkerAlt.d.tsfaMapMarkerAlt.jsfaMapPin.d.tsfaMapPin.jsfaMapSigns.d.tsfaMapSigns.jsfaMars.d.tsfaMars.jsfaMarsDouble.d.tsfaMarsDouble.jsfaMarsStroke.d.tsfaMarsStroke.jsfaMarsStrokeH.d.tsfaMarsStrokeH.jsfaMarsStrokeV.d.tsfaMarsStrokeV.jsfaMedkit.d.tsfaMedkit.jsfaMeh.d.tsfaMeh.jsfaMemory.d.tsfaMemory.jsfaMercury.d.tsfaMercury.jsfaMicrochip.d.tsfaMicrochip.jsfaMicrophone.d.tsfaMicrophone.jsfaMicrophoneAlt.d.tsfaMicrophoneAlt.jsfaMicrophoneAltSlash.d.tsfaMicrophoneAltSlash.jsfaMicrophoneSlash.d.tsfaMicrophoneSlash.jsfaMinus.d.tsfaMinus.jsfaMinusCircle.d.tsfaMinusCircle.jsfaMinusSquare.d.tsfaMinusSquare.jsfaMobile.d.tsfaMobile.jsfaMobileAlt.d.tsfaMobileAlt.jsfaMoneyBill.d.tsfaMoneyBill.jsfaMoneyBillAlt.d.tsfaMoneyBillAlt.jsfaMoneyBillWave.d.tsfaMoneyBillWave.jsfaMoneyBillWaveAlt.d.tsfaMoneyBillWaveAlt.jsfaMoneyCheck.d.tsfaMoneyCheck.jsfaMoneyCheckAlt.d.tsfaMoneyCheckAlt.jsfaMoon.d.tsfaMoon.jsfaMotorcycle.d.tsfaMotorcycle.jsfaMousePointer.d.tsfaMousePointer.jsfaMusic.d.tsfaMusic.jsfaNeuter.d.tsfaNeuter.jsfaNewspaper.d.tsfaNewspaper.jsfaNotEqual.d.tsfaNotEqual.jsfaNotesMedical.d.tsfaNotesMedical.jsfaObjectGroup.d.tsfaObjectGroup.jsfaObjectUngroup.d.tsfaObjectUngroup.jsfaOutdent.d.tsfaOutdent.jsfaPaintBrush.d.tsfaPaintBrush.jsfaPalette.d.tsfaPalette.jsfaPallet.d.tsfaPallet.jsfaPaperPlane.d.tsfaPaperPlane.jsfaPaperclip.d.tsfaPaperclip.jsfaParachuteBox.d.tsfaParachuteBox.jsfaParagraph.d.tsfaParagraph.jsfaParking.d.tsfaParking.jsfaPaste.d.tsfaPaste.jsfaPause.d.tsfaPause.jsfaPauseCircle.d.tsfaPauseCircle.jsfaPaw.d.tsfaPaw.jsfaPenSquare.d.tsfaPenSquare.jsfaPencilAlt.d.tsfaPencilAlt.jsfaPeopleCarry.d.tsfaPeopleCarry.jsfaPercent.d.tsfaPercent.jsfaPercentage.d.tsfaPercentage.jsfaPhone.d.tsfaPhone.jsfaPhoneSlash.d.tsfaPhoneSlash.jsfaPhoneSquare.d.tsfaPhoneSquare.jsfaPhoneVolume.d.tsfaPhoneVolume.jsfaPiggyBank.d.tsfaPiggyBank.jsfaPills.d.tsfaPills.jsfaPlane.d.tsfaPlane.jsfaPlay.d.tsfaPlay.jsfaPlayCircle.d.tsfaPlayCircle.jsfaPlug.d.tsfaPlug.jsfaPlus.d.tsfaPlus.jsfaPlusCircle.d.tsfaPlusCircle.jsfaPlusSquare.d.tsfaPlusSquare.jsfaPodcast.d.tsfaPodcast.jsfaPoo.d.tsfaPoo.jsfaPortrait.d.tsfaPortrait.jsfaPoundSign.d.tsfaPoundSign.jsfaPowerOff.d.tsfaPowerOff.jsfaPrescriptionBottle.d.tsfaPrescriptionBottle.jsfaPrescriptionBottleAlt.d.tsfaPrescriptionBottleAlt.jsfaPrint.d.tsfaPrint.jsfaProcedures.d.tsfaProcedures.jsfaProjectDiagram.d.tsfaProjectDiagram.jsfaPuzzlePiece.d.tsfaPuzzlePiece.jsfaQrcode.d.tsfaQrcode.jsfaQuestion.d.tsfaQuestion.jsfaQuestionCircle.d.tsfaQuestionCircle.jsfaQuidditch.d.tsfaQuidditch.jsfaQuoteLeft.d.tsfaQuoteLeft.jsfaQuoteRight.d.tsfaQuoteRight.jsfaRandom.d.tsfaRandom.jsfaReceipt.d.tsfaReceipt.jsfaRecycle.d.tsfaRecycle.jsfaRedo.d.tsfaRedo.jsfaRedoAlt.d.tsfaRedoAlt.jsfaRegistered.d.tsfaRegistered.jsfaReply.d.tsfaReply.jsfaReplyAll.d.tsfaReplyAll.jsfaRetweet.d.tsfaRetweet.jsfaRibbon.d.tsfaRibbon.jsfaRoad.d.tsfaRoad.jsfaRobot.d.tsfaRobot.jsfaRocket.d.tsfaRocket.jsfaRss.d.tsfaRss.jsfaRssSquare.d.tsfaRssSquare.jsfaRubleSign.d.tsfaRubleSign.jsfaRuler.d.tsfaRuler.jsfaRulerCombined.d.tsfaRulerCombined.jsfaRulerHorizontal.d.tsfaRulerHorizontal.jsfaRulerVertical.d.tsfaRulerVertical.jsfaRupeeSign.d.tsfaRupeeSign.jsfaSave.d.tsfaSave.jsfaSchool.d.tsfaSchool.jsfaScrewdriver.d.tsfaScrewdriver.jsfaSearch.d.tsfaSearch.jsfaSearchMinus.d.tsfaSearchMinus.jsfaSearchPlus.d.tsfaSearchPlus.jsfaSeedling.d.tsfaSeedling.jsfaServer.d.tsfaServer.jsfaShare.d.tsfaShare.jsfaShareAlt.d.tsfaShareAlt.jsfaShareAltSquare.d.tsfaShareAltSquare.jsfaShareSquare.d.tsfaShareSquare.jsfaShekelSign.d.tsfaShekelSign.jsfaShieldAlt.d.tsfaShieldAlt.jsfaShip.d.tsfaShip.jsfaShippingFast.d.tsfaShippingFast.jsfaShoePrints.d.tsfaShoePrints.jsfaShoppingBag.d.tsfaShoppingBag.jsfaShoppingBasket.d.tsfaShoppingBasket.jsfaShoppingCart.d.tsfaShoppingCart.jsfaShower.d.tsfaShower.jsfaSign.d.tsfaSign.jsfaSignInAlt.d.tsfaSignInAlt.jsfaSignLanguage.d.tsfaSignLanguage.jsfaSignOutAlt.d.tsfaSignOutAlt.jsfaSignal.d.tsfaSignal.jsfaSitemap.d.tsfaSitemap.jsfaSkull.d.tsfaSkull.jsfaSlidersH.d.tsfaSlidersH.jsfaSmile.d.tsfaSmile.jsfaSmoking.d.tsfaSmoking.jsfaSmokingBan.d.tsfaSmokingBan.jsfaSnowflake.d.tsfaSnowflake.jsfaSort.d.tsfaSort.jsfaSortAlphaDown.d.tsfaSortAlphaDown.jsfaSortAlphaUp.d.tsfaSortAlphaUp.jsfaSortAmountDown.d.tsfaSortAmountDown.jsfaSortAmountUp.d.tsfaSortAmountUp.jsfaSortDown.d.tsfaSortDown.jsfaSortNumericDown.d.tsfaSortNumericDown.jsfaSortNumericUp.d.tsfaSortNumericUp.jsfaSortUp.d.tsfaSortUp.jsfaSpaceShuttle.d.tsfaSpaceShuttle.jsfaSpinner.d.tsfaSpinner.jsfaSquare.d.tsfaSquare.jsfaSquareFull.d.tsfaSquareFull.jsfaStar.d.tsfaStar.jsfaStarHalf.d.tsfaStarHalf.jsfaStepBackward.d.tsfaStepBackward.jsfaStepForward.d.tsfaStepForward.jsfaStethoscope.d.tsfaStethoscope.jsfaStickyNote.d.tsfaStickyNote.jsfaStop.d.tsfaStop.jsfaStopCircle.d.tsfaStopCircle.jsfaStopwatch.d.tsfaStopwatch.jsfaStore.d.tsfaStore.jsfaStoreAlt.d.tsfaStoreAlt.jsfaStream.d.tsfaStream.jsfaStreetView.d.tsfaStreetView.jsfaStrikethrough.d.tsfaStrikethrough.jsfaStroopwafel.d.tsfaStroopwafel.jsfaSubscript.d.tsfaSubscript.jsfaSubway.d.tsfaSubway.jsfaSuitcase.d.tsfaSuitcase.jsfaSun.d.tsfaSun.jsfaSuperscript.d.tsfaSuperscript.jsfaSync.d.tsfaSync.jsfaSyncAlt.d.tsfaSyncAlt.jsfaSyringe.d.tsfaSyringe.jsfaTable.d.tsfaTable.jsfaTableTennis.d.tsfaTableTennis.jsfaTablet.d.tsfaTablet.jsfaTabletAlt.d.tsfaTabletAlt.jsfaTablets.d.tsfaTablets.jsfaTachometerAlt.d.tsfaTachometerAlt.jsfaTag.d.tsfaTag.jsfaTags.d.tsfaTags.jsfaTape.d.tsfaTape.jsfaTasks.d.tsfaTasks.jsfaTaxi.d.tsfaTaxi.jsfaTerminal.d.tsfaTerminal.jsfaTextHeight.d.tsfaTextHeight.jsfaTextWidth.d.tsfaTextWidth.jsfaTh.d.tsfaTh.jsfaThLarge.d.tsfaThLarge.jsfaThList.d.tsfaThList.jsfaThermometer.d.tsfaThermometer.jsfaThermometerEmpty.d.tsfaThermometerEmpty.jsfaThermometerFull.d.tsfaThermometerFull.jsfaThermometerHalf.d.tsfaThermometerHalf.jsfaThermometerQuarter.d.tsfaThermometerQuarter.jsfaThermometerThreeQuarters.d.tsfaThermometerThreeQuarters.jsfaThumbsDown.d.tsfaThumbsDown.jsfaThumbsUp.d.tsfaThumbsUp.jsfaThumbtack.d.tsfaThumbtack.jsfaTicketAlt.d.tsfaTicketAlt.jsfaTimes.d.tsfaTimes.jsfaTimesCircle.d.tsfaTimesCircle.jsfaTint.d.tsfaTint.jsfaToggleOff.d.tsfaToggleOff.jsfaToggleOn.d.tsfaToggleOn.jsfaToolbox.d.tsfaToolbox.jsfaTrademark.d.tsfaTrademark.jsfaTrain.d.tsfaTrain.jsfaTransgender.d.tsfaTransgender.jsfaTransgenderAlt.d.tsfaTransgenderAlt.jsfaTrash.d.tsfaTrash.jsfaTrashAlt.d.tsfaTrashAlt.jsfaTree.d.tsfaTree.jsfaTrophy.d.tsfaTrophy.jsfaTruck.d.tsfaTruck.jsfaTruckLoading.d.tsfaTruckLoading.jsfaTruckMoving.d.tsfaTruckMoving.jsfaTshirt.d.tsfaTshirt.jsfaTty.d.tsfaTty.jsfaTv.d.tsfaTv.jsfaUmbrella.d.tsfaUmbrella.jsfaUnderline.d.tsfaUnderline.jsfaUndo.d.tsfaUndo.jsfaUndoAlt.d.tsfaUndoAlt.jsfaUniversalAccess.d.tsfaUniversalAccess.jsfaUniversity.d.tsfaUniversity.jsfaUnlink.d.tsfaUnlink.jsfaUnlock.d.tsfaUnlock.jsfaUnlockAlt.d.tsfaUnlockAlt.jsfaUpload.d.tsfaUpload.jsfaUser.d.tsfaUser.jsfaUserAlt.d.tsfaUserAlt.jsfaUserAltSlash.d.tsfaUserAltSlash.jsfaUserAstronaut.d.tsfaUserAstronaut.jsfaUserCheck.d.tsfaUserCheck.jsfaUserCircle.d.tsfaUserCircle.jsfaUserClock.d.tsfaUserClock.jsfaUserCog.d.tsfaUserCog.jsfaUserEdit.d.tsfaUserEdit.jsfaUserFriends.d.tsfaUserFriends.jsfaUserGraduate.d.tsfaUserGraduate.jsfaUserLock.d.tsfaUserLock.jsfaUserMd.d.tsfaUserMd.jsfaUserMinus.d.tsfaUserMinus.jsfaUserNinja.d.tsfaUserNinja.jsfaUserPlus.d.tsfaUserPlus.jsfaUserSecret.d.tsfaUserSecret.jsfaUserShield.d.tsfaUserShield.jsfaUserSlash.d.tsfaUserSlash.jsfaUserTag.d.tsfaUserTag.jsfaUserTie.d.tsfaUserTie.jsfaUserTimes.d.tsfaUserTimes.jsfaUsers.d.tsfaUsers.jsfaUsersCog.d.tsfaUsersCog.jsfaUtensilSpoon.d.tsfaUtensilSpoon.jsfaUtensils.d.tsfaUtensils.jsfaVenus.d.tsfaVenus.jsfaVenusDouble.d.tsfaVenusDouble.jsfaVenusMars.d.tsfaVenusMars.jsfaVial.d.tsfaVial.jsfaVials.d.tsfaVials.jsfaVideo.d.tsfaVideo.jsfaVideoSlash.d.tsfaVideoSlash.jsfaVolleyballBall.d.tsfaVolleyballBall.jsfaVolumeDown.d.tsfaVolumeDown.jsfaVolumeOff.d.tsfaVolumeOff.jsfaVolumeUp.d.tsfaVolumeUp.jsfaWalking.d.tsfaWalking.jsfaWallet.d.tsfaWallet.jsfaWarehouse.d.tsfaWarehouse.jsfaWeight.d.tsfaWeight.jsfaWheelchair.d.tsfaWheelchair.jsfaWifi.d.tsfaWifi.jsfaWindowClose.d.tsfaWindowClose.jsfaWindowMaximize.d.tsfaWindowMaximize.jsfaWindowMinimize.d.tsfaWindowMinimize.jsfaWindowRestore.d.tsfaWindowRestore.jsfaWineGlass.d.tsfaWineGlass.jsfaWonSign.d.tsfaWonSign.jsfaWrench.d.tsfaWrench.jsfaXRay.d.tsfaXRay.jsfaYenSign.d.tsfaYenSign.jsindex.d.tsindex.es.jsindex.jspackage.jsonshakable.es.jsshakable.js
fontawesome-free-webfonts
fontawesome-free
LICENSE.txtREADME.md
css
js
less
package.json
scss
sprites
svgs
brands
500px.svgaccessible-icon.svgaccusoft.svgadn.svgadversal.svgaffiliatetheme.svgalgolia.svgamazon-pay.svgamazon.svgamilia.svgandroid.svgangellist.svgangrycreative.svgangular.svgapp-store-ios.svgapp-store.svgapper.svgapple-pay.svgapple.svgasymmetrik.svgaudible.svgautoprefixer.svgavianex.svgaviato.svgaws.svgbandcamp.svgbehance-square.svgbehance.svgbimobject.svgbitbucket.svgbitcoin.svgbity.svgblack-tie.svgblackberry.svgblogger-b.svgblogger.svgbluetooth-b.svgbluetooth.svgbtc.svgburomobelexperte.svgbuysellads.svgcc-amazon-pay.svgcc-amex.svgcc-apple-pay.svgcc-diners-club.svgcc-discover.svgcc-jcb.svgcc-mastercard.svgcc-paypal.svgcc-stripe.svgcc-visa.svgcentercode.svgchrome.svgcloudscale.svgcloudsmith.svgcloudversify.svgcodepen.svgcodiepie.svgconnectdevelop.svgcontao.svgcpanel.svgcreative-commons-by.svgcreative-commons-nc-eu.svgcreative-commons-nc-jp.svgcreative-commons-nc.svgcreative-commons-nd.svgcreative-commons-pd-alt.svgcreative-commons-pd.svgcreative-commons-remix.svgcreative-commons-sa.svgcreative-commons-sampling-plus.svgcreative-commons-sampling.svgcreative-commons-share.svgcreative-commons.svgcss3-alt.svgcss3.svgcuttlefish.svgd-and-d.svgdashcube.svgdelicious.svgdeploydog.svgdeskpro.svgdeviantart.svgdigg.svgdigital-ocean.svgdiscord.svgdiscourse.svgdochub.svgdocker.svgdraft2digital.svgdribbble-square.svgdribbble.svgdropbox.svgdrupal.svgdyalog.svgearlybirds.svgebay.svgedge.svgelementor.svgember.svgempire.svgenvira.svgerlang.svgethereum.svgetsy.svgexpeditedssl.svgfacebook-f.svgfacebook-messenger.svgfacebook-square.svgfacebook.svgfirefox.svgfirst-order-alt.svgfirst-order.svgfirstdraft.svgflickr.svgflipboard.svgfly.svgfont-awesome-alt.svgfont-awesome-flag.svgfont-awesome-logo-full.svgfont-awesome.svgfonticons-fi.svgfonticons.svgfort-awesome-alt.svgfort-awesome.svgforumbee.svgfoursquare.svgfree-code-camp.svgfreebsd.svgfulcrum.svggalactic-republic.svggalactic-senate.svgget-pocket.svggg-circle.svggg.svggit-square.svggit.svggithub-alt.svggithub-square.svggithub.svggitkraken.svggitlab.svggitter.svgglide-g.svgglide.svggofore.svggoodreads-g.svggoodreads.svggoogle-drive.svggoogle-play.svggoogle-plus-g.svggoogle-plus-square.svggoogle-plus.svggoogle-wallet.svggoogle.svggratipay.svggrav.svggripfire.svggrunt.svggulp.svghacker-news-square.svghacker-news.svghips.svghire-a-helper.svghooli.svghornbill.svghotjar.svghouzz.svghtml5.svghubspot.svgimdb.svginstagram.svginternet-explorer.svgioxhost.svgitunes-note.svgitunes.svgjava.svgjedi-order.svgjenkins.svgjoget.svgjoomla.svgjs-square.svgjs.svgjsfiddle.svgkeybase.svgkeycdn.svgkickstarter-k.svgkickstarter.svgkorvue.svglaravel.svglastfm-square.svglastfm.svgleanpub.svgless.svgline.svglinkedin-in.svglinkedin.svglinode.svglinux.svglyft.svgmagento.svgmailchimp.svgmandalorian.svgmastodon.svgmaxcdn.svgmedapps.svgmedium-m.svgmedium.svgmedrt.svgmeetup.svgmegaport.svgmicrosoft.svgmix.svgmixcloud.svgmizuni.svgmodx.svgmonero.svgnapster.svgnimblr.svgnintendo-switch.svgnode-js.svgnode.svgnpm.svgns8.svgnutritionix.svgodnoklassniki-square.svgodnoklassniki.svgold-republic.svgopencart.svgopenid.svgopera.svgoptin-monster.svgosi.svgpage4.svgpagelines.svgpalfed.svgpatreon.svgpaypal.svgperiscope.svgphabricator.svgphoenix-framework.svgphoenix-squadron.svgphp.svgpied-piper-alt.svgpied-piper-hat.svgpied-piper-pp.svgpied-piper.svgpinterest-p.svgpinterest-square.svgpinterest.svgplaystation.svgproduct-hunt.svgpushed.svgpython.svgqq.svgquinscape.svgquora.svgr-project.svgravelry.svgreact.svgreadme.svgrebel.svgred-river.svgreddit-alien.svgreddit-square.svgreddit.svgrendact.svgrenren.svgreplyd.svgresearchgate.svgresolving.svgrev.svgrocketchat.svgrockrms.svgsafari.svgsass.svgschlix.svgscribd.svgsearchengin.svgsellcast.svgsellsy.svgservicestack.svgshirtsinbulk.svgshopware.svgsimplybuilt.svgsistrix.svgsith.svgskyatlas.svgskype.svgslack-hash.svgslack.svgslideshare.svgsnapchat-ghost.svgsnapchat-square.svgsnapchat.svgsoundcloud.svgspeakap.svgspotify.svgsquarespace.svgstack-exchange.svgstack-overflow.svgstaylinked.svgsteam-square.svgsteam-symbol.svgsteam.svgsticker-mule.svgstrava.svgstripe-s.svgstripe.svgstudiovinari.svgstumbleupon-circle.svgstumbleupon.svgsuperpowers.svgsupple.svgteamspeak.svgtelegram-plane.svgtelegram.svgtencent-weibo.svgthemeco.svgthemeisle.svgtrade-federation.svgtrello.svgtripadvisor.svgtumblr-square.svgtumblr.svgtwitch.svgtwitter-square.svgtwitter.svgtypo3.svguber.svguikit.svguniregistry.svguntappd.svgusb.svgussunnah.svgvaadin.svgviacoin.svgviadeo-square.svgviadeo.svgviber.svgvimeo-square.svgvimeo-v.svgvimeo.svgvine.svgvk.svgvnv.svgvuejs.svgweebly.svgweibo.svgweixin.svgwhatsapp-square.svgwhatsapp.svgwhmcs.svgwikipedia-w.svgwindows.svgwix.svgwolf-pack-battalion.svgwordpress-simple.svgwordpress.svgwpbeginner.svgwpexplorer.svgwpforms.svgxbox.svgxing-square.svgxing.svgy-combinator.svgyahoo.svgyandex-international.svgyandex.svgyelp.svgyoast.svgyoutube-square.svgyoutube.svg
regular
address-book.svgaddress-card.svgangry.svgarrow-alt-circle-down.svgarrow-alt-circle-left.svgarrow-alt-circle-right.svgarrow-alt-circle-up.svgbell-slash.svgbell.svgbookmark.svgbuilding.svgcalendar-alt.svgcalendar-check.svgcalendar-minus.svgcalendar-plus.svgcalendar-times.svgcalendar.svgcaret-square-down.svgcaret-square-left.svgcaret-square-right.svgcaret-square-up.svgchart-bar.svgcheck-circle.svgcheck-square.svgcircle.svgclipboard.svgclock.svgclone.svgclosed-captioning.svgcomment-alt.svgcomment-dots.svgcomment.svgcomments.svgcompass.svgcopy.svgcopyright.svgcredit-card.svgdizzy.svgdot-circle.svgedit.svgenvelope-open.svgenvelope.svgeye-slash.svgeye.svgfile-alt.svgfile-archive.svgfile-audio.svgfile-code.svgfile-excel.svgfile-image.svgfile-pdf.svgfile-powerpoint.svgfile-video.svgfile-word.svgfile.svgflag.svgflushed.svgfolder-open.svgfolder.svgfont-awesome-logo-full.svgfrown-open.svgfrown.svgfutbol.svggem.svggrimace.svggrin-alt.svggrin-beam-sweat.svggrin-beam.svggrin-hearts.svggrin-squint-tears.svggrin-squint.svggrin-stars.svggrin-tears.svggrin-tongue-squint.svggrin-tongue-wink.svggrin-tongue.svggrin-wink.svggrin.svghand-lizard.svghand-paper.svghand-peace.svghand-point-down.svghand-point-left.svghand-point-right.svghand-point-up.svghand-pointer.svghand-rock.svghand-scissors.svghand-spock.svghandshake.svghdd.svgheart.svghospital.svghourglass.svgid-badge.svgid-card.svgimage.svgimages.svgkeyboard.svgkiss-beam.svgkiss-wink-heart.svgkiss.svglaugh-beam.svglaugh-squint.svglaugh-wink.svglaugh.svglemon.svglife-ring.svglightbulb.svglist-alt.svgmap.svgmeh-blank.svgmeh-rolling-eyes.svgmeh.svgminus-square.svgmoney-bill-alt.svgmoon.svgnewspaper.svgobject-group.svgobject-ungroup.svgpaper-plane.svgpause-circle.svgplay-circle.svgplus-square.svgquestion-circle.svgregistered.svgsad-cry.svgsad-tear.svgsave.svgshare-square.svgsmile-beam.svgsmile-wink.svgsmile.svgsnowflake.svgsquare.svgstar-half.svgstar.svgsticky-note.svgstop-circle.svgsun.svgsurprise.svgthumbs-down.svgthumbs-up.svgtimes-circle.svgtired.svgtrash-alt.svguser-circle.svguser.svgwindow-close.svgwindow-maximize.svgwindow-minimize.svgwindow-restore.svg
solid
address-book.svgaddress-card.svgadjust.svgalign-center.svgalign-justify.svgalign-left.svgalign-right.svgallergies.svgambulance.svgamerican-sign-language-interpreting.svganchor.svgangle-double-down.svgangle-double-left.svgangle-double-right.svgangle-double-up.svgangle-down.svgangle-left.svgangle-right.svgangle-up.svgangry.svgarchive.svgarchway.svgarrow-alt-circle-down.svgarrow-alt-circle-left.svgarrow-alt-circle-right.svgarrow-alt-circle-up.svgarrow-circle-down.svgarrow-circle-left.svgarrow-circle-right.svgarrow-circle-up.svgarrow-down.svgarrow-left.svgarrow-right.svgarrow-up.svgarrows-alt-h.svgarrows-alt-v.svgarrows-alt.svgassistive-listening-systems.svgasterisk.svgat.svgatlas.svgaudio-description.svgaward.svgbackspace.svgbackward.svgbalance-scale.svgban.svgband-aid.svgbarcode.svgbars.svgbaseball-ball.svgbasketball-ball.svgbath.svgbattery-empty.svgbattery-full.svgbattery-half.svgbattery-quarter.svgbattery-three-quarters.svgbed.svgbeer.svgbell-slash.svgbell.svgbezier-curve.svgbicycle.svgbinoculars.svgbirthday-cake.svgblender.svgblind.svgbold.svgbolt.svgbomb.svgbong.svgbook-open.svgbook.svgbookmark.svgbowling-ball.svgbox-open.svgbox.svgboxes.svgbraille.svgbriefcase-medical.svgbriefcase.svgbroadcast-tower.svgbroom.svgbrush.svgbug.svgbuilding.svgbullhorn.svgbullseye.svgburn.svgbus-alt.svgbus.svgcalculator.svgcalendar-alt.svgcalendar-check.svgcalendar-minus.svgcalendar-plus.svgcalendar-times.svgcalendar.svgcamera-retro.svgcamera.svgcannabis.svgcapsules.svgcar.svgcaret-down.svgcaret-left.svgcaret-right.svgcaret-square-down.svgcaret-square-left.svgcaret-square-right.svgcaret-square-up.svgcaret-up.svgcart-arrow-down.svgcart-plus.svgcertificate.svgchalkboard-teacher.svgchalkboard.svgchart-area.svgchart-bar.svgchart-line.svgchart-pie.svgcheck-circle.svgcheck-double.svgcheck-square.svgcheck.svgchess-bishop.svgchess-board.svgchess-king.svgchess-knight.svgchess-pawn.svgchess-queen.svgchess-rook.svgchess.svgchevron-circle-down.svgchevron-circle-left.svgchevron-circle-right.svgchevron-circle-up.svgchevron-down.svgchevron-left.svgchevron-right.svgchevron-up.svgchild.svgchurch.svgcircle-notch.svgcircle.svgclipboard-check.svgclipboard-list.svgclipboard.svgclock.svgclone.svgclosed-captioning.svgcloud-download-alt.svgcloud-upload-alt.svgcloud.svgcocktail.svgcode-branch.svgcode.svgcoffee.svgcog.svgcogs.svgcoins.svgcolumns.svgcomment-alt.svgcomment-dots.svgcomment-slash.svgcomment.svgcomments.svgcompact-disc.svgcompass.svgcompress.svgconcierge-bell.svgcookie-bite.svgcookie.svgcopy.svgcopyright.svgcouch.svgcredit-card.svgcrop-alt.svgcrop.svgcrosshairs.svgcrow.svgcrown.svgcube.svgcubes.svgcut.svgdatabase.svgdeaf.svgdesktop.svgdiagnoses.svgdice-five.svgdice-four.svgdice-one.svgdice-six.svgdice-three.svgdice-two.svgdice.svgdigital-tachograph.svgdivide.svgdizzy.svgdna.svgdollar-sign.svgdolly-flatbed.svgdolly.svgdonate.svgdoor-closed.svgdoor-open.svgdot-circle.svgdove.svgdownload.svgdrafting-compass.svgdrum-steelpan.svgdrum.svgdumbbell.svgedit.svgeject.svgellipsis-h.svgellipsis-v.svgenvelope-open.svgenvelope-square.svgenvelope.svgequals.svgeraser.svgeuro-sign.svgexchange-alt.svgexclamation-circle.svgexclamation-triangle.svgexclamation.svgexpand-arrows-alt.svgexpand.svgexternal-link-alt.svgexternal-link-square-alt.svgeye-dropper.svgeye-slash.svgeye.svgfast-backward.svgfast-forward.svgfax.svgfeather-alt.svgfeather.svgfemale.svgfighter-jet.svgfile-alt.svgfile-archive.svgfile-audio.svgfile-code.svgfile-contract.svgfile-download.svgfile-excel.svgfile-export.svgfile-image.svgfile-import.svgfile-invoice-dollar.svgfile-invoice.svgfile-medical-alt.svgfile-medical.svgfile-pdf.svgfile-powerpoint.svgfile-prescription.svgfile-signature.svgfile-upload.svgfile-video.svgfile-word.svgfile.svgfill-drip.svgfill.svgfilm.svgfilter.svgfingerprint.svgfire-extinguisher.svgfire.svgfirst-aid.svgfish.svgflag-checkered.svgflag.svgflask.svgflushed.svgfolder-open.svgfolder.svgfont-awesome-logo-full.svgfont.svgfootball-ball.svgforward.svgfrog.svgfrown-open.svgfrown.svgfutbol.svggamepad.svggas-pump.svggavel.svggem.svggenderless.svggift.svgglass-martini-alt.svgglass-martini.svgglasses.svgglobe-africa.svgglobe-americas.svgglobe-asia.svgglobe.svggolf-ball.svggraduation-cap.svggreater-than-equal.svggreater-than.svggrimace.svggrin-alt.svggrin-beam-sweat.svggrin-beam.svggrin-hearts.svggrin-squint-tears.svggrin-squint.svggrin-stars.svggrin-tears.svggrin-tongue-squint.svggrin-tongue-wink.svggrin-tongue.svggrin-wink.svggrin.svggrip-horizontal.svggrip-vertical.svgh-square.svghand-holding-heart.svghand-holding-usd.svghand-holding.svghand-lizard.svghand-paper.svghand-peace.svghand-point-down.svghand-point-left.svghand-point-right.svghand-point-up.svghand-pointer.svghand-rock.svghand-scissors.svghand-spock.svghands-helping.svghands.svghandshake.svghashtag.svghdd.svgheading.svgheadphones-alt.svgheadphones.svgheadset.svgheart.svgheartbeat.svghelicopter.svghighlighter.svghistory.svghockey-puck.svghome.svghospital-alt.svghospital-symbol.svghospital.svghot-tub.svghotel.svghourglass-end.svghourglass-half.svghourglass-start.svghourglass.svgi-cursor.svgid-badge.svgid-card-alt.svgid-card.svgimage.svgimages.svginbox.svgindent.svgindustry.svginfinity.svginfo-circle.svginfo.svgitalic.svgjoint.svgkey.svgkeyboard.svgkiss-beam.svgkiss-wink-heart.svgkiss.svgkiwi-bird.svglanguage.svglaptop.svglaugh-beam.svglaugh-squint.svglaugh-wink.svglaugh.svgleaf.svglemon.svgless-than-equal.svgless-than.svglevel-down-alt.svglevel-up-alt.svglife-ring.svglightbulb.svglink.svglira-sign.svglist-alt.svglist-ol.svglist-ul.svglist.svglocation-arrow.svglock-open.svglock.svglong-arrow-alt-down.svglong-arrow-alt-left.svglong-arrow-alt-right.svglong-arrow-alt-up.svglow-vision.svgluggage-cart.svgmagic.svgmagnet.svgmale.svgmap-marked-alt.svgmap-marked.svgmap-marker-alt.svgmap-marker.svgmap-pin.svgmap-signs.svgmap.svgmarker.svgmars-double.svgmars-stroke-h.svgmars-stroke-v.svgmars-stroke.svgmars.svgmedal.svgmedkit.svgmeh-blank.svgmeh-rolling-eyes.svgmeh.svgmemory.svgmercury.svgmicrochip.svgmicrophone-alt-slash.svgmicrophone-alt.svgmicrophone-slash.svgmicrophone.svgminus-circle.svgminus-square.svgminus.svgmobile-alt.svgmobile.svgmoney-bill-alt.svgmoney-bill-wave-alt.svgmoney-bill-wave.svgmoney-bill.svgmoney-check-alt.svgmoney-check.svgmonument.svgmoon.svgmortar-pestle.svgmotorcycle.svgmouse-pointer.svgmusic.svgneuter.svgnewspaper.svgnot-equal.svgnotes-medical.svgobject-group.svgobject-ungroup.svgoutdent.svgpaint-brush.svgpaint-roller.svgpalette.svgpallet.svgpaper-plane.svgpaperclip.svgparachute-box.svgparagraph.svgparking.svgpassport.svgpaste.svgpause-circle.svgpause.svgpaw.svgpen-alt.svgpen-fancy.svgpen-nib.svgpen-square.svgpen.svgpencil-alt.svgpencil-ruler.svgpeople-carry.svgpercent.svgpercentage.svgphone-slash.svgphone-square.svgphone-volume.svgphone.svgpiggy-bank.svgpills.svgplane-arrival.svgplane-departure.svgplane.svgplay-circle.svgplay.svgplug.svgplus-circle.svgplus-square.svgplus.svgpodcast.svgpoo.svgportrait.svgpound-sign.svgpower-off.svgprescription-bottle-alt.svgprescription-bottle.svgprescription.svgprint.svgprocedures.svgproject-diagram.svgpuzzle-piece.svgqrcode.svgquestion-circle.svgquestion.svgquidditch.svgquote-left.svgquote-right.svgrandom.svgreceipt.svgrecycle.svgredo-alt.svgredo.svgregistered.svgreply-all.svgreply.svgretweet.svgribbon.svgroad.svgrobot.svgrocket.svgrss-square.svgrss.svgruble-sign.svgruler-combined.svgruler-horizontal.svgruler-vertical.svgruler.svgrupee-sign.svgsad-cry.svgsad-tear.svgsave.svgschool.svgscrewdriver.svgsearch-minus.svgsearch-plus.svgsearch.svgseedling.svgserver.svgshare-alt-square.svgshare-alt.svgshare-square.svgshare.svgshekel-sign.svgshield-alt.svgship.svgshipping-fast.svgshoe-prints.svgshopping-bag.svgshopping-basket.svgshopping-cart.svgshower.svgshuttle-van.svgsign-in-alt.svgsign-language.svgsign-out-alt.svgsign.svgsignal.svgsignature.svgsitemap.svgskull.svgsliders-h.svgsmile-beam.svgsmile-wink.svgsmile.svgsmoking-ban.svgsmoking.svgsnowflake.svgsolar-panel.svgsort-alpha-down.svgsort-alpha-up.svgsort-amount-down.svgsort-amount-up.svgsort-down.svgsort-numeric-down.svgsort-numeric-up.svgsort-up.svgsort.svgspa.svgspace-shuttle.svgspinner.svgsplotch.svgspray-can.svgsquare-full.svgsquare.svgstamp.svgstar-half-alt.svgstar-half.svgstar.svgstep-backward.svgstep-forward.svgstethoscope.svgsticky-note.svgstop-circle.svgstop.svgstopwatch.svgstore-alt.svgstore.svgstream.svgstreet-view.svgstrikethrough.svgstroopwafel.svgsubscript.svgsubway.svgsuitcase-rolling.svgsuitcase.svgsun.svgsuperscript.svgsurprise.svgswatchbook.svgswimmer.svgswimming-pool.svgsync-alt.svgsync.svgsyringe.svgtable-tennis.svgtable.svgtablet-alt.svgtablet.svgtablets.svgtachometer-alt.svgtag.svgtags.svgtape.svgtasks.svgtaxi.svgterminal.svgtext-height.svgtext-width.svgth-large.svgth-list.svgth.svgthermometer-empty.svgthermometer-full.svgthermometer-half.svgthermometer-quarter.svgthermometer-three-quarters.svgthermometer.svgthumbs-down.svgthumbs-up.svgthumbtack.svgticket-alt.svgtimes-circle.svgtimes.svgtint-slash.svgtint.svgtired.svgtoggle-off.svgtoggle-on.svgtoolbox.svgtooth.svgtrademark.svgtrain.svgtransgender-alt.svgtransgender.svgtrash-alt.svgtrash.svgtree.svgtrophy.svgtruck-loading.svgtruck-moving.svgtruck.svgtshirt.svgtty.svgtv.svgumbrella-beach.svgumbrella.svgunderline.svgundo-alt.svgundo.svguniversal-access.svguniversity.svgunlink.svgunlock-alt.svgunlock.svgupload.svguser-alt-slash.svguser-alt.svguser-astronaut.svguser-check.svguser-circle.svguser-clock.svguser-cog.svguser-edit.svguser-friends.svguser-graduate.svguser-lock.svguser-md.svguser-minus.svguser-ninja.svguser-plus.svguser-secret.svguser-shield.svguser-slash.svguser-tag.svguser-tie.svguser-times.svguser.svgusers-cog.svgusers.svgutensil-spoon.svgutensils.svgvector-square.svgvenus-double.svgvenus-mars.svgvenus.svgvial.svgvials.svgvideo-slash.svgvideo.svgvolleyball-ball.svgvolume-down.svgvolume-off.svgvolume-up.svgwalking.svgwallet.svgwarehouse.svgweight-hanging.svgweight.svgwheelchair.svgwifi.svgwindow-close.svgwindow-maximize.svgwindow-minimize.svgwindow-restore.svgwine-glass-alt.svgwine-glass.svgwon-sign.svgwrench.svgx-ray.svgyen-sign.svg
webfonts
fontawesome-svg-core
fontawesome
free-brands-svg-icons
LICENSE.txtREADME.mdfa500px.d.tsfa500px.jsfaAccessibleIcon.d.tsfaAccessibleIcon.jsfaAccusoft.d.tsfaAccusoft.jsfaAdn.d.tsfaAdn.jsfaAdversal.d.tsfaAdversal.jsfaAffiliatetheme.d.tsfaAffiliatetheme.jsfaAlgolia.d.tsfaAlgolia.jsfaAmazon.d.tsfaAmazon.jsfaAmazonPay.d.tsfaAmazonPay.jsfaAmilia.d.tsfaAmilia.jsfaAndroid.d.tsfaAndroid.jsfaAngellist.d.tsfaAngellist.jsfaAngrycreative.d.tsfaAngrycreative.jsfaAngular.d.tsfaAngular.jsfaAppStore.d.tsfaAppStore.jsfaAppStoreIos.d.tsfaAppStoreIos.jsfaApper.d.tsfaApper.jsfaApple.d.tsfaApple.jsfaApplePay.d.tsfaApplePay.jsfaAsymmetrik.d.tsfaAsymmetrik.jsfaAudible.d.tsfaAudible.jsfaAutoprefixer.d.tsfaAutoprefixer.jsfaAvianex.d.tsfaAvianex.jsfaAviato.d.tsfaAviato.jsfaAws.d.tsfaAws.jsfaBandcamp.d.tsfaBandcamp.jsfaBehance.d.tsfaBehance.jsfaBehanceSquare.d.tsfaBehanceSquare.jsfaBimobject.d.tsfaBimobject.jsfaBitbucket.d.tsfaBitbucket.jsfaBitcoin.d.tsfaBitcoin.jsfaBity.d.tsfaBity.jsfaBlackTie.d.tsfaBlackTie.jsfaBlackberry.d.tsfaBlackberry.jsfaBlogger.d.tsfaBlogger.jsfaBloggerB.d.tsfaBloggerB.jsfaBluetooth.d.tsfaBluetooth.jsfaBluetoothB.d.tsfaBluetoothB.jsfaBtc.d.tsfaBtc.jsfaBuromobelexperte.d.tsfaBuromobelexperte.jsfaBuysellads.d.tsfaBuysellads.jsfaCcAmazonPay.d.tsfaCcAmazonPay.jsfaCcAmex.d.tsfaCcAmex.jsfaCcApplePay.d.tsfaCcApplePay.jsfaCcDinersClub.d.tsfaCcDinersClub.jsfaCcDiscover.d.tsfaCcDiscover.jsfaCcJcb.d.tsfaCcJcb.jsfaCcMastercard.d.tsfaCcMastercard.jsfaCcPaypal.d.tsfaCcPaypal.jsfaCcStripe.d.tsfaCcStripe.jsfaCcVisa.d.tsfaCcVisa.jsfaCentercode.d.tsfaCentercode.jsfaChrome.d.tsfaChrome.jsfaCloudscale.d.tsfaCloudscale.jsfaCloudsmith.d.tsfaCloudsmith.jsfaCloudversify.d.tsfaCloudversify.jsfaCodepen.d.tsfaCodepen.jsfaCodiepie.d.tsfaCodiepie.jsfaConnectdevelop.d.tsfaConnectdevelop.jsfaContao.d.tsfaContao.jsfaCpanel.d.tsfaCpanel.jsfaCreativeCommons.d.tsfaCreativeCommons.jsfaCreativeCommonsBy.d.tsfaCreativeCommonsBy.jsfaCreativeCommonsNc.d.tsfaCreativeCommonsNc.jsfaCreativeCommonsNcEu.d.tsfaCreativeCommonsNcEu.jsfaCreativeCommonsNcJp.d.tsfaCreativeCommonsNcJp.jsfaCreativeCommonsNd.d.tsfaCreativeCommonsNd.jsfaCreativeCommonsPd.d.tsfaCreativeCommonsPd.jsfaCreativeCommonsPdAlt.d.tsfaCreativeCommonsPdAlt.jsfaCreativeCommonsRemix.d.tsfaCreativeCommonsRemix.jsfaCreativeCommonsSa.d.tsfaCreativeCommonsSa.jsfaCreativeCommonsSampling.d.tsfaCreativeCommonsSampling.jsfaCreativeCommonsSamplingPlus.d.tsfaCreativeCommonsSamplingPlus.jsfaCreativeCommonsShare.d.tsfaCreativeCommonsShare.jsfaCss3.d.tsfaCss3.jsfaCss3Alt.d.tsfaCss3Alt.jsfaCuttlefish.d.tsfaCuttlefish.jsfaDAndD.d.tsfaDAndD.jsfaDashcube.d.tsfaDashcube.jsfaDelicious.d.tsfaDelicious.jsfaDeploydog.d.tsfaDeploydog.jsfaDeskpro.d.tsfaDeskpro.jsfaDeviantart.d.tsfaDeviantart.jsfaDigg.d.tsfaDigg.jsfaDigitalOcean.d.tsfaDigitalOcean.jsfaDiscord.d.tsfaDiscord.jsfaDiscourse.d.tsfaDiscourse.jsfaDochub.d.tsfaDochub.jsfaDocker.d.tsfaDocker.jsfaDraft2digital.d.tsfaDraft2digital.jsfaDribbble.d.tsfaDribbble.jsfaDribbbleSquare.d.tsfaDribbbleSquare.jsfaDropbox.d.tsfaDropbox.jsfaDrupal.d.tsfaDrupal.jsfaDyalog.d.tsfaDyalog.jsfaEarlybirds.d.tsfaEarlybirds.jsfaEbay.d.tsfaEbay.jsfaEdge.d.tsfaEdge.jsfaElementor.d.tsfaElementor.jsfaEmber.d.tsfaEmber.jsfaEmpire.d.tsfaEmpire.jsfaEnvira.d.tsfaEnvira.jsfaErlang.d.tsfaErlang.jsfaEthereum.d.tsfaEthereum.jsfaEtsy.d.tsfaEtsy.jsfaExpeditedssl.d.tsfaExpeditedssl.jsfaFacebook.d.tsfaFacebook.jsfaFacebookF.d.tsfaFacebookF.jsfaFacebookMessenger.d.tsfaFacebookMessenger.jsfaFacebookSquare.d.tsfaFacebookSquare.jsfaFirefox.d.tsfaFirefox.jsfaFirstOrder.d.tsfaFirstOrder.jsfaFirstOrderAlt.d.tsfaFirstOrderAlt.jsfaFirstdraft.d.tsfaFirstdraft.jsfaFlickr.d.tsfaFlickr.jsfaFlipboard.d.tsfaFlipboard.jsfaFly.d.tsfaFly.jsfaFontAwesome.d.tsfaFontAwesome.jsfaFontAwesomeAlt.d.tsfaFontAwesomeAlt.jsfaFontAwesomeFlag.d.tsfaFontAwesomeFlag.jsfaFontAwesomeLogoFull.d.tsfaFontAwesomeLogoFull.jsfaFonticons.d.tsfaFonticons.jsfaFonticonsFi.d.tsfaFonticonsFi.jsfaFortAwesome.d.tsfaFortAwesome.jsfaFortAwesomeAlt.d.tsfaFortAwesomeAlt.jsfaForumbee.d.tsfaForumbee.jsfaFoursquare.d.tsfaFoursquare.jsfaFreeCodeCamp.d.tsfaFreeCodeCamp.jsfaFreebsd.d.tsfaFreebsd.jsfaFulcrum.d.tsfaFulcrum.jsfaGalacticRepublic.d.tsfaGalacticRepublic.jsfaGalacticSenate.d.tsfaGalacticSenate.jsfaGetPocket.d.tsfaGetPocket.jsfaGg.d.tsfaGg.jsfaGgCircle.d.tsfaGgCircle.jsfaGit.d.tsfaGit.jsfaGitSquare.d.tsfaGitSquare.jsfaGithub.d.tsfaGithub.jsfaGithubAlt.d.tsfaGithubAlt.jsfaGithubSquare.d.tsfaGithubSquare.jsfaGitkraken.d.tsfaGitkraken.jsfaGitlab.d.tsfaGitlab.jsfaGitter.d.tsfaGitter.jsfaGlide.d.tsfaGlide.jsfaGlideG.d.tsfaGlideG.jsfaGofore.d.tsfaGofore.jsfaGoodreads.d.tsfaGoodreads.jsfaGoodreadsG.d.tsfaGoodreadsG.jsfaGoogle.d.tsfaGoogle.jsfaGoogleDrive.d.tsfaGoogleDrive.jsfaGooglePlay.d.tsfaGooglePlay.jsfaGooglePlus.d.tsfaGooglePlus.jsfaGooglePlusG.d.tsfaGooglePlusG.jsfaGooglePlusSquare.d.tsfaGooglePlusSquare.jsfaGoogleWallet.d.tsfaGoogleWallet.jsfaGratipay.d.tsfaGratipay.jsfaGrav.d.tsfaGrav.jsfaGripfire.d.tsfaGripfire.jsfaGrunt.d.tsfaGrunt.jsfaGulp.d.tsfaGulp.jsfaHackerNews.d.tsfaHackerNews.jsfaHackerNewsSquare.d.tsfaHackerNewsSquare.jsfaHips.d.tsfaHips.jsfaHireAHelper.d.tsfaHireAHelper.jsfaHooli.d.tsfaHooli.jsfaHornbill.d.tsfaHornbill.jsfaHotjar.d.tsfaHotjar.jsfaHouzz.d.tsfaHouzz.jsfaHtml5.d.tsfaHtml5.jsfaHubspot.d.tsfaHubspot.jsfaImdb.d.tsfaImdb.jsfaInstagram.d.tsfaInstagram.jsfaInternetExplorer.d.tsfaInternetExplorer.jsfaIoxhost.d.tsfaIoxhost.jsfaItunes.d.tsfaItunes.jsfaItunesNote.d.tsfaItunesNote.jsfaJava.d.tsfaJava.jsfaJediOrder.d.tsfaJediOrder.jsfaJenkins.d.tsfaJenkins.jsfaJoget.d.tsfaJoget.jsfaJoomla.d.tsfaJoomla.jsfaJs.d.tsfaJs.jsfaJsSquare.d.tsfaJsSquare.jsfaJsfiddle.d.tsfaJsfiddle.jsfaKeybase.d.tsfaKeybase.jsfaKeycdn.d.tsfaKeycdn.jsfaKickstarter.d.tsfaKickstarter.jsfaKickstarterK.d.tsfaKickstarterK.jsfaKorvue.d.tsfaKorvue.jsfaLaravel.d.tsfaLaravel.jsfaLastfm.d.tsfaLastfm.jsfaLastfmSquare.d.tsfaLastfmSquare.jsfaLeanpub.d.tsfaLeanpub.jsfaLess.d.tsfaLess.jsfaLine.d.tsfaLine.jsfaLinkedin.d.tsfaLinkedin.jsfaLinkedinIn.d.tsfaLinkedinIn.jsfaLinode.d.tsfaLinode.jsfaLinux.d.tsfaLinux.jsfaLyft.d.tsfaLyft.jsfaMagento.d.tsfaMagento.jsfaMailchimp.d.tsfaMailchimp.jsfaMandalorian.d.tsfaMandalorian.jsfaMastodon.d.tsfaMastodon.jsfaMaxcdn.d.tsfaMaxcdn.jsfaMedapps.d.tsfaMedapps.jsfaMedium.d.tsfaMedium.jsfaMediumM.d.tsfaMediumM.jsfaMedrt.d.tsfaMedrt.jsfaMeetup.d.tsfaMeetup.jsfaMegaport.d.tsfaMegaport.jsfaMicrosoft.d.tsfaMicrosoft.jsfaMix.d.tsfaMix.jsfaMixcloud.d.tsfaMixcloud.jsfaMizuni.d.tsfaMizuni.jsfaModx.d.tsfaModx.jsfaMonero.d.tsfaMonero.jsfaNapster.d.tsfaNapster.jsfaNimblr.d.tsfaNimblr.jsfaNintendoSwitch.d.tsfaNintendoSwitch.jsfaNode.d.tsfaNode.jsfaNodeJs.d.tsfaNodeJs.jsfaNpm.d.tsfaNpm.jsfaNs8.d.tsfaNs8.jsfaNutritionix.d.tsfaNutritionix.jsfaOdnoklassniki.d.tsfaOdnoklassniki.jsfaOdnoklassnikiSquare.d.tsfaOdnoklassnikiSquare.jsfaOldRepublic.d.tsfaOldRepublic.jsfaOpencart.d.tsfaOpencart.jsfaOpenid.d.tsfaOpenid.jsfaOpera.d.tsfaOpera.jsfaOptinMonster.d.tsfaOptinMonster.jsfaOsi.d.tsfaOsi.jsfaPage4.d.tsfaPage4.jsfaPagelines.d.tsfaPagelines.jsfaPalfed.d.tsfaPalfed.jsfaPatreon.d.tsfaPatreon.jsfaPaypal.d.tsfaPaypal.jsfaPeriscope.d.tsfaPeriscope.jsfaPhabricator.d.tsfaPhabricator.jsfaPhoenixFramework.d.tsfaPhoenixFramework.jsfaPhoenixSquadron.d.tsfaPhoenixSquadron.jsfaPhp.d.tsfaPhp.jsfaPiedPiper.d.tsfaPiedPiper.jsfaPiedPiperAlt.d.tsfaPiedPiperAlt.jsfaPiedPiperHat.d.tsfaPiedPiperHat.jsfaPiedPiperPp.d.tsfaPiedPiperPp.jsfaPinterest.d.tsfaPinterest.jsfaPinterestP.d.tsfaPinterestP.jsfaPinterestSquare.d.tsfaPinterestSquare.jsfaPlaystation.d.tsfaPlaystation.jsfaProductHunt.d.tsfaProductHunt.jsfaPushed.d.tsfaPushed.jsfaPython.d.tsfaPython.jsfaQq.d.tsfaQq.jsfaQuinscape.d.tsfaQuinscape.jsfaQuora.d.tsfaQuora.jsfaRProject.d.tsfaRProject.jsfaRavelry.d.tsfaRavelry.jsfaReact.d.tsfaReact.jsfaReadme.d.tsfaReadme.jsfaRebel.d.tsfaRebel.jsfaRedRiver.d.tsfaRedRiver.jsfaReddit.d.tsfaReddit.jsfaRedditAlien.d.tsfaRedditAlien.jsfaRedditSquare.d.tsfaRedditSquare.jsfaRendact.d.tsfaRendact.jsfaRenren.d.tsfaRenren.jsfaReplyd.d.tsfaReplyd.jsfaResearchgate.d.tsfaResearchgate.jsfaResolving.d.tsfaResolving.jsfaRev.d.tsfaRev.jsfaRocketchat.d.tsfaRocketchat.jsfaRockrms.d.tsfaRockrms.jsfaSafari.d.tsfaSafari.jsfaSass.d.tsfaSass.jsfaSchlix.d.tsfaSchlix.jsfaScribd.d.tsfaScribd.jsfaSearchengin.d.tsfaSearchengin.jsfaSellcast.d.tsfaSellcast.jsfaSellsy.d.tsfaSellsy.jsfaServicestack.d.tsfaServicestack.jsfaShirtsinbulk.d.tsfaShirtsinbulk.jsfaShopware.d.tsfaShopware.jsfaSimplybuilt.d.tsfaSimplybuilt.jsfaSistrix.d.tsfaSistrix.jsfaSith.d.tsfaSith.jsfaSkyatlas.d.tsfaSkyatlas.jsfaSkype.d.tsfaSkype.jsfaSlack.d.tsfaSlack.jsfaSlackHash.d.tsfaSlackHash.jsfaSlideshare.d.tsfaSlideshare.jsfaSnapchat.d.tsfaSnapchat.jsfaSnapchatGhost.d.tsfaSnapchatGhost.jsfaSnapchatSquare.d.tsfaSnapchatSquare.jsfaSoundcloud.d.tsfaSoundcloud.jsfaSpeakap.d.tsfaSpeakap.jsfaSpotify.d.tsfaSpotify.jsfaSquarespace.d.tsfaSquarespace.jsfaStackExchange.d.tsfaStackExchange.jsfaStackOverflow.d.tsfaStackOverflow.jsfaStaylinked.d.tsfaStaylinked.jsfaSteam.d.tsfaSteam.jsfaSteamSquare.d.tsfaSteamSquare.jsfaSteamSymbol.d.tsfaSteamSymbol.jsfaStickerMule.d.tsfaStickerMule.jsfaStrava.d.tsfaStrava.jsfaStripe.d.tsfaStripe.jsfaStripeS.d.tsfaStripeS.jsfaStudiovinari.d.tsfaStudiovinari.jsfaStumbleupon.d.tsfaStumbleupon.jsfaStumbleuponCircle.d.tsfaStumbleuponCircle.jsfaSuperpowers.d.tsfaSuperpowers.jsfaSupple.d.tsfaSupple.jsfaTeamspeak.d.tsfaTeamspeak.jsfaTelegram.d.tsfaTelegram.jsfaTelegramPlane.d.tsfaTelegramPlane.jsfaTencentWeibo.d.tsfaTencentWeibo.jsfaThemeco.d.tsfaThemeco.jsfaThemeisle.d.tsfaThemeisle.jsfaTradeFederation.d.tsfaTradeFederation.jsfaTrello.d.tsfaTrello.jsfaTripadvisor.d.tsfaTripadvisor.jsfaTumblr.d.tsfaTumblr.jsfaTumblrSquare.d.tsfaTumblrSquare.jsfaTwitch.d.tsfaTwitch.jsfaTwitter.d.tsfaTwitter.jsfaTwitterSquare.d.tsfaTwitterSquare.jsfaTypo3.d.tsfaTypo3.jsfaUber.d.tsfaUber.jsfaUikit.d.tsfaUikit.jsfaUniregistry.d.tsfaUniregistry.jsfaUntappd.d.tsfaUntappd.jsfaUsb.d.tsfaUsb.jsfaUssunnah.d.tsfaUssunnah.jsfaVaadin.d.tsfaVaadin.jsfaViacoin.d.tsfaViacoin.jsfaViadeo.d.tsfaViadeo.jsfaViadeoSquare.d.tsfaViadeoSquare.jsfaViber.d.tsfaViber.jsfaVimeo.d.tsfaVimeo.jsfaVimeoSquare.d.tsfaVimeoSquare.jsfaVimeoV.d.tsfaVimeoV.jsfaVine.d.tsfaVine.jsfaVk.d.tsfaVk.jsfaVnv.d.tsfaVnv.jsfaVuejs.d.tsfaVuejs.jsfaWeebly.d.tsfaWeebly.jsfaWeibo.d.tsfaWeibo.jsfaWeixin.d.tsfaWeixin.jsfaWhatsapp.d.tsfaWhatsapp.jsfaWhatsappSquare.d.tsfaWhatsappSquare.jsfaWhmcs.d.tsfaWhmcs.jsfaWikipediaW.d.tsfaWikipediaW.jsfaWindows.d.tsfaWindows.jsfaWix.d.tsfaWix.jsfaWolfPackBattalion.d.tsfaWolfPackBattalion.jsfaWordpress.d.tsfaWordpress.jsfaWordpressSimple.d.tsfaWordpressSimple.jsfaWpbeginner.d.tsfaWpbeginner.jsfaWpexplorer.d.tsfaWpexplorer.jsfaWpforms.d.tsfaWpforms.jsfaXbox.d.tsfaXbox.jsfaXing.d.tsfaXing.jsfaXingSquare.d.tsfaXingSquare.jsfaYCombinator.d.tsfaYCombinator.jsfaYahoo.d.tsfaYahoo.jsfaYandex.d.tsfaYandex.jsfaYandexInternational.d.tsfaYandexInternational.jsfaYelp.d.tsfaYelp.jsfaYoast.d.tsfaYoast.jsfaYoutube.d.tsfaYoutube.jsfaYoutubeSquare.d.tsfaYoutubeSquare.jsindex.d.tsindex.es.jsindex.jspackage.json
free-regular-svg-icons
LICENSE.txtREADME.mdfaAddressBook.d.tsfaAddressBook.jsfaAddressCard.d.tsfaAddressCard.jsfaAngry.d.tsfaAngry.jsfaArrowAltCircleDown.d.tsfaArrowAltCircleDown.jsfaArrowAltCircleLeft.d.tsfaArrowAltCircleLeft.jsfaArrowAltCircleRight.d.tsfaArrowAltCircleRight.jsfaArrowAltCircleUp.d.tsfaArrowAltCircleUp.jsfaBell.d.tsfaBell.jsfaBellSlash.d.tsfaBellSlash.jsfaBookmark.d.tsfaBookmark.jsfaBuilding.d.tsfaBuilding.jsfaCalendar.d.tsfaCalendar.jsfaCalendarAlt.d.tsfaCalendarAlt.jsfaCalendarCheck.d.tsfaCalendarCheck.jsfaCalendarMinus.d.tsfaCalendarMinus.jsfaCalendarPlus.d.tsfaCalendarPlus.jsfaCalendarTimes.d.tsfaCalendarTimes.jsfaCaretSquareDown.d.tsfaCaretSquareDown.jsfaCaretSquareLeft.d.tsfaCaretSquareLeft.jsfaCaretSquareRight.d.tsfaCaretSquareRight.jsfaCaretSquareUp.d.tsfaCaretSquareUp.jsfaChartBar.d.tsfaChartBar.jsfaCheckCircle.d.tsfaCheckCircle.jsfaCheckSquare.d.tsfaCheckSquare.jsfaCircle.d.tsfaCircle.jsfaClipboard.d.tsfaClipboard.jsfaClock.d.tsfaClock.jsfaClone.d.tsfaClone.jsfaClosedCaptioning.d.tsfaClosedCaptioning.jsfaComment.d.tsfaComment.jsfaCommentAlt.d.tsfaCommentAlt.jsfaCommentDots.d.tsfaCommentDots.jsfaComments.d.tsfaComments.jsfaCompass.d.tsfaCompass.jsfaCopy.d.tsfaCopy.jsfaCopyright.d.tsfaCopyright.jsfaCreditCard.d.tsfaCreditCard.jsfaDizzy.d.tsfaDizzy.jsfaDotCircle.d.tsfaDotCircle.jsfaEdit.d.tsfaEdit.jsfaEnvelope.d.tsfaEnvelope.jsfaEnvelopeOpen.d.tsfaEnvelopeOpen.jsfaEye.d.tsfaEye.jsfaEyeSlash.d.tsfaEyeSlash.jsfaFile.d.tsfaFile.jsfaFileAlt.d.tsfaFileAlt.jsfaFileArchive.d.tsfaFileArchive.jsfaFileAudio.d.tsfaFileAudio.jsfaFileCode.d.tsfaFileCode.jsfaFileExcel.d.tsfaFileExcel.jsfaFileImage.d.tsfaFileImage.jsfaFilePdf.d.tsfaFilePdf.jsfaFilePowerpoint.d.tsfaFilePowerpoint.jsfaFileVideo.d.tsfaFileVideo.jsfaFileWord.d.tsfaFileWord.jsfaFlag.d.tsfaFlag.jsfaFlushed.d.tsfaFlushed.jsfaFolder.d.tsfaFolder.jsfaFolderOpen.d.tsfaFolderOpen.jsfaFontAwesomeLogoFull.d.tsfaFontAwesomeLogoFull.jsfaFrown.d.tsfaFrown.jsfaFrownOpen.d.tsfaFrownOpen.jsfaFutbol.d.tsfaFutbol.jsfaGem.d.tsfaGem.jsfaGrimace.d.tsfaGrimace.jsfaGrin.d.tsfaGrin.jsfaGrinAlt.d.tsfaGrinAlt.jsfaGrinBeam.d.tsfaGrinBeam.jsfaGrinBeamSweat.d.tsfaGrinBeamSweat.jsfaGrinHearts.d.tsfaGrinHearts.jsfaGrinSquint.d.tsfaGrinSquint.jsfaGrinSquintTears.d.tsfaGrinSquintTears.jsfaGrinStars.d.tsfaGrinStars.jsfaGrinTears.d.tsfaGrinTears.jsfaGrinTongue.d.tsfaGrinTongue.jsfaGrinTongueSquint.d.tsfaGrinTongueSquint.jsfaGrinTongueWink.d.tsfaGrinTongueWink.jsfaGrinWink.d.tsfaGrinWink.jsfaHandLizard.d.tsfaHandLizard.jsfaHandPaper.d.tsfaHandPaper.jsfaHandPeace.d.tsfaHandPeace.jsfaHandPointDown.d.tsfaHandPointDown.jsfaHandPointLeft.d.tsfaHandPointLeft.jsfaHandPointRight.d.tsfaHandPointRight.jsfaHandPointUp.d.tsfaHandPointUp.jsfaHandPointer.d.tsfaHandPointer.jsfaHandRock.d.tsfaHandRock.jsfaHandScissors.d.tsfaHandScissors.jsfaHandSpock.d.tsfaHandSpock.jsfaHandshake.d.tsfaHandshake.jsfaHdd.d.tsfaHdd.jsfaHeart.d.tsfaHeart.jsfaHospital.d.tsfaHospital.jsfaHourglass.d.tsfaHourglass.jsfaIdBadge.d.tsfaIdBadge.jsfaIdCard.d.tsfaIdCard.jsfaImage.d.tsfaImage.jsfaImages.d.tsfaImages.jsfaKeyboard.d.tsfaKeyboard.jsfaKiss.d.tsfaKiss.jsfaKissBeam.d.tsfaKissBeam.jsfaKissWinkHeart.d.tsfaKissWinkHeart.jsfaLaugh.d.tsfaLaugh.jsfaLaughBeam.d.tsfaLaughBeam.jsfaLaughSquint.d.tsfaLaughSquint.jsfaLaughWink.d.tsfaLaughWink.jsfaLemon.d.tsfaLemon.jsfaLifeRing.d.tsfaLifeRing.jsfaLightbulb.d.tsfaLightbulb.jsfaListAlt.d.tsfaListAlt.jsfaMap.d.tsfaMap.jsfaMeh.d.tsfaMeh.jsfaMehBlank.d.tsfaMehBlank.jsfaMehRollingEyes.d.tsfaMehRollingEyes.jsfaMinusSquare.d.tsfaMinusSquare.jsfaMoneyBillAlt.d.tsfaMoneyBillAlt.jsfaMoon.d.tsfaMoon.jsfaNewspaper.d.tsfaNewspaper.jsfaObjectGroup.d.tsfaObjectGroup.jsfaObjectUngroup.d.tsfaObjectUngroup.jsfaPaperPlane.d.tsfaPaperPlane.jsfaPauseCircle.d.tsfaPauseCircle.jsfaPlayCircle.d.tsfaPlayCircle.jsfaPlusSquare.d.tsfaPlusSquare.jsfaQuestionCircle.d.tsfaQuestionCircle.jsfaRegistered.d.tsfaRegistered.jsfaSadCry.d.tsfaSadCry.jsfaSadTear.d.tsfaSadTear.jsfaSave.d.tsfaSave.jsfaShareSquare.d.tsfaShareSquare.jsfaSmile.d.tsfaSmile.jsfaSmileBeam.d.tsfaSmileBeam.jsfaSmileWink.d.tsfaSmileWink.jsfaSnowflake.d.tsfaSnowflake.jsfaSquare.d.tsfaSquare.jsfaStar.d.tsfaStar.jsfaStarHalf.d.tsfaStarHalf.jsfaStickyNote.d.tsfaStickyNote.jsfaStopCircle.d.tsfaStopCircle.jsfaSun.d.tsfaSun.jsfaSurprise.d.tsfaSurprise.jsfaThumbsDown.d.tsfaThumbsDown.jsfaThumbsUp.d.tsfaThumbsUp.jsfaTimesCircle.d.tsfaTimesCircle.jsfaTired.d.tsfaTired.jsfaTrashAlt.d.tsfaTrashAlt.jsfaUser.d.tsfaUser.jsfaUserCircle.d.tsfaUserCircle.jsfaWindowClose.d.tsfaWindowClose.jsfaWindowMaximize.d.tsfaWindowMaximize.jsfaWindowMinimize.d.tsfaWindowMinimize.jsfaWindowRestore.d.tsfaWindowRestore.jsindex.d.tsindex.es.jsindex.jspackage.json
free-solid-svg-icons
LICENSE.txtREADME.mdfaAddressBook.d.tsfaAddressBook.jsfaAddressCard.d.tsfaAddressCard.jsfaAdjust.d.tsfaAdjust.jsfaAlignCenter.d.tsfaAlignCenter.jsfaAlignJustify.d.tsfaAlignJustify.jsfaAlignLeft.d.tsfaAlignLeft.jsfaAlignRight.d.tsfaAlignRight.jsfaAllergies.d.tsfaAllergies.jsfaAmbulance.d.tsfaAmbulance.jsfaAmericanSignLanguageInterpreting.d.tsfaAmericanSignLanguageInterpreting.jsfaAnchor.d.tsfaAnchor.jsfaAngleDoubleDown.d.tsfaAngleDoubleDown.jsfaAngleDoubleLeft.d.tsfaAngleDoubleLeft.jsfaAngleDoubleRight.d.tsfaAngleDoubleRight.jsfaAngleDoubleUp.d.tsfaAngleDoubleUp.jsfaAngleDown.d.tsfaAngleDown.jsfaAngleLeft.d.tsfaAngleLeft.jsfaAngleRight.d.tsfaAngleRight.jsfaAngleUp.d.tsfaAngleUp.jsfaAngry.d.tsfaAngry.jsfaArchive.d.tsfaArchive.jsfaArchway.d.tsfaArchway.jsfaArrowAltCircleDown.d.tsfaArrowAltCircleDown.jsfaArrowAltCircleLeft.d.tsfaArrowAltCircleLeft.jsfaArrowAltCircleRight.d.tsfaArrowAltCircleRight.jsfaArrowAltCircleUp.d.tsfaArrowAltCircleUp.jsfaArrowCircleDown.d.tsfaArrowCircleDown.jsfaArrowCircleLeft.d.tsfaArrowCircleLeft.jsfaArrowCircleRight.d.tsfaArrowCircleRight.jsfaArrowCircleUp.d.tsfaArrowCircleUp.jsfaArrowDown.d.tsfaArrowDown.jsfaArrowLeft.d.tsfaArrowLeft.jsfaArrowRight.d.tsfaArrowRight.jsfaArrowUp.d.tsfaArrowUp.jsfaArrowsAlt.d.tsfaArrowsAlt.jsfaArrowsAltH.d.tsfaArrowsAltH.jsfaArrowsAltV.d.tsfaArrowsAltV.jsfaAssistiveListeningSystems.d.tsfaAssistiveListeningSystems.jsfaAsterisk.d.tsfaAsterisk.jsfaAt.d.tsfaAt.jsfaAtlas.d.tsfaAtlas.jsfaAudioDescription.d.tsfaAudioDescription.jsfaAward.d.tsfaAward.jsfaBackspace.d.tsfaBackspace.jsfaBackward.d.tsfaBackward.jsfaBalanceScale.d.tsfaBalanceScale.jsfaBan.d.tsfaBan.jsfaBandAid.d.tsfaBandAid.jsfaBarcode.d.tsfaBarcode.jsfaBars.d.tsfaBars.jsfaBaseballBall.d.tsfaBaseballBall.jsfaBasketballBall.d.tsfaBasketballBall.jsfaBath.d.tsfaBath.jsfaBatteryEmpty.d.tsfaBatteryEmpty.jsfaBatteryFull.d.tsfaBatteryFull.jsfaBatteryHalf.d.tsfaBatteryHalf.jsfaBatteryQuarter.d.tsfaBatteryQuarter.jsfaBatteryThreeQuarters.d.tsfaBatteryThreeQuarters.jsfaBed.d.tsfaBed.jsfaBeer.d.tsfaBeer.jsfaBell.d.tsfaBell.jsfaBellSlash.d.tsfaBellSlash.jsfaBezierCurve.d.tsfaBezierCurve.jsfaBicycle.d.tsfaBicycle.jsfaBinoculars.d.tsfaBinoculars.jsfaBirthdayCake.d.tsfaBirthdayCake.jsfaBlender.d.tsfaBlender.jsfaBlind.d.tsfaBlind.jsfaBold.d.tsfaBold.jsfaBolt.d.tsfaBolt.jsfaBomb.d.tsfaBomb.jsfaBong.d.tsfaBong.jsfaBook.d.tsfaBook.jsfaBookOpen.d.tsfaBookOpen.jsfaBookmark.d.tsfaBookmark.jsfaBowlingBall.d.tsfaBowlingBall.jsfaBox.d.tsfaBox.jsfaBoxOpen.d.tsfaBoxOpen.jsfaBoxes.d.tsfaBoxes.jsfaBraille.d.tsfaBraille.jsfaBriefcase.d.tsfaBriefcase.jsfaBriefcaseMedical.d.tsfaBriefcaseMedical.jsfaBroadcastTower.d.tsfaBroadcastTower.jsfaBroom.d.tsfaBroom.jsfaBrush.d.tsfaBrush.jsfaBug.d.tsfaBug.jsfaBuilding.d.tsfaBuilding.jsfaBullhorn.d.tsfaBullhorn.jsfaBullseye.d.tsfaBullseye.jsfaBurn.d.tsfaBurn.jsfaBus.d.tsfaBus.jsfaBusAlt.d.tsfaBusAlt.jsfaCalculator.d.tsfaCalculator.jsfaCalendar.d.tsfaCalendar.jsfaCalendarAlt.d.tsfaCalendarAlt.jsfaCalendarCheck.d.tsfaCalendarCheck.jsfaCalendarMinus.d.tsfaCalendarMinus.jsfaCalendarPlus.d.tsfaCalendarPlus.jsfaCalendarTimes.d.tsfaCalendarTimes.jsfaCamera.d.tsfaCamera.jsfaCameraRetro.d.tsfaCameraRetro.jsfaCannabis.d.tsfaCannabis.jsfaCapsules.d.tsfaCapsules.jsfaCar.d.tsfaCar.jsfaCaretDown.d.tsfaCaretDown.jsfaCaretLeft.d.tsfaCaretLeft.jsfaCaretRight.d.tsfaCaretRight.jsfaCaretSquareDown.d.tsfaCaretSquareDown.jsfaCaretSquareLeft.d.tsfaCaretSquareLeft.jsfaCaretSquareRight.d.tsfaCaretSquareRight.jsfaCaretSquareUp.d.tsfaCaretSquareUp.jsfaCaretUp.d.tsfaCaretUp.jsfaCartArrowDown.d.tsfaCartArrowDown.jsfaCartPlus.d.tsfaCartPlus.jsfaCertificate.d.tsfaCertificate.jsfaChalkboard.d.tsfaChalkboard.jsfaChalkboardTeacher.d.tsfaChalkboardTeacher.jsfaChartArea.d.tsfaChartArea.jsfaChartBar.d.tsfaChartBar.jsfaChartLine.d.tsfaChartLine.jsfaChartPie.d.tsfaChartPie.jsfaCheck.d.tsfaCheck.jsfaCheckCircle.d.tsfaCheckCircle.jsfaCheckDouble.d.tsfaCheckDouble.jsfaCheckSquare.d.tsfaCheckSquare.jsfaChess.d.tsfaChess.jsfaChessBishop.d.tsfaChessBishop.jsfaChessBoard.d.tsfaChessBoard.jsfaChessKing.d.tsfaChessKing.jsfaChessKnight.d.tsfaChessKnight.jsfaChessPawn.d.tsfaChessPawn.jsfaChessQueen.d.tsfaChessQueen.jsfaChessRook.d.tsfaChessRook.jsfaChevronCircleDown.d.tsfaChevronCircleDown.jsfaChevronCircleLeft.d.tsfaChevronCircleLeft.jsfaChevronCircleRight.d.tsfaChevronCircleRight.jsfaChevronCircleUp.d.tsfaChevronCircleUp.jsfaChevronDown.d.tsfaChevronDown.jsfaChevronLeft.d.tsfaChevronLeft.jsfaChevronRight.d.tsfaChevronRight.jsfaChevronUp.d.tsfaChevronUp.jsfaChild.d.tsfaChild.jsfaChurch.d.tsfaChurch.jsfaCircle.d.tsfaCircle.jsfaCircleNotch.d.tsfaCircleNotch.jsfaClipboard.d.tsfaClipboard.jsfaClipboardCheck.d.tsfaClipboardCheck.jsfaClipboardList.d.tsfaClipboardList.jsfaClock.d.tsfaClock.jsfaClone.d.tsfaClone.jsfaClosedCaptioning.d.tsfaClosedCaptioning.jsfaCloud.d.tsfaCloud.jsfaCloudDownloadAlt.d.tsfaCloudDownloadAlt.jsfaCloudUploadAlt.d.tsfaCloudUploadAlt.jsfaCocktail.d.tsfaCocktail.jsfaCode.d.tsfaCode.jsfaCodeBranch.d.tsfaCodeBranch.jsfaCoffee.d.tsfaCoffee.jsfaCog.d.tsfaCog.jsfaCogs.d.tsfaCogs.jsfaCoins.d.tsfaCoins.jsfaColumns.d.tsfaColumns.jsfaComment.d.tsfaComment.jsfaCommentAlt.d.tsfaCommentAlt.jsfaCommentDots.d.tsfaCommentDots.jsfaCommentSlash.d.tsfaCommentSlash.jsfaComments.d.tsfaComments.jsfaCompactDisc.d.tsfaCompactDisc.jsfaCompass.d.tsfaCompass.jsfaCompress.d.tsfaCompress.jsfaConciergeBell.d.tsfaConciergeBell.jsfaCookie.d.tsfaCookie.jsfaCookieBite.d.tsfaCookieBite.jsfaCopy.d.tsfaCopy.jsfaCopyright.d.tsfaCopyright.jsfaCouch.d.tsfaCouch.jsfaCreditCard.d.tsfaCreditCard.jsfaCrop.d.tsfaCrop.jsfaCropAlt.d.tsfaCropAlt.jsfaCrosshairs.d.tsfaCrosshairs.jsfaCrow.d.tsfaCrow.jsfaCrown.d.tsfaCrown.jsfaCube.d.tsfaCube.jsfaCubes.d.tsfaCubes.jsfaCut.d.tsfaCut.jsfaDatabase.d.tsfaDatabase.jsfaDeaf.d.tsfaDeaf.jsfaDesktop.d.tsfaDesktop.jsfaDiagnoses.d.tsfaDiagnoses.jsfaDice.d.tsfaDice.jsfaDiceFive.d.tsfaDiceFive.jsfaDiceFour.d.tsfaDiceFour.jsfaDiceOne.d.tsfaDiceOne.jsfaDiceSix.d.tsfaDiceSix.jsfaDiceThree.d.tsfaDiceThree.jsfaDiceTwo.d.tsfaDiceTwo.jsfaDigitalTachograph.d.tsfaDigitalTachograph.jsfaDivide.d.tsfaDivide.jsfaDizzy.d.tsfaDizzy.jsfaDna.d.tsfaDna.jsfaDollarSign.d.tsfaDollarSign.jsfaDolly.d.tsfaDolly.jsfaDollyFlatbed.d.tsfaDollyFlatbed.jsfaDonate.d.tsfaDonate.jsfaDoorClosed.d.tsfaDoorClosed.jsfaDoorOpen.d.tsfaDoorOpen.jsfaDotCircle.d.tsfaDotCircle.jsfaDove.d.tsfaDove.jsfaDownload.d.tsfaDownload.jsfaDraftingCompass.d.tsfaDraftingCompass.jsfaDrum.d.tsfaDrum.jsfaDrumSteelpan.d.tsfaDrumSteelpan.jsfaDumbbell.d.tsfaDumbbell.jsfaEdit.d.tsfaEdit.jsfaEject.d.tsfaEject.jsfaEllipsisH.d.tsfaEllipsisH.jsfaEllipsisV.d.tsfaEllipsisV.jsfaEnvelope.d.tsfaEnvelope.jsfaEnvelopeOpen.d.tsfaEnvelopeOpen.jsfaEnvelopeSquare.d.tsfaEnvelopeSquare.jsfaEquals.d.tsfaEquals.jsfaEraser.d.tsfaEraser.jsfaEuroSign.d.tsfaEuroSign.jsfaExchangeAlt.d.tsfaExchangeAlt.jsfaExclamation.d.tsfaExclamation.jsfaExclamationCircle.d.tsfaExclamationCircle.jsfaExclamationTriangle.d.tsfaExclamationTriangle.jsfaExpand.d.tsfaExpand.jsfaExpandArrowsAlt.d.tsfaExpandArrowsAlt.jsfaExternalLinkAlt.d.tsfaExternalLinkAlt.jsfaExternalLinkSquareAlt.d.tsfaExternalLinkSquareAlt.jsfaEye.d.tsfaEye.jsfaEyeDropper.d.tsfaEyeDropper.jsfaEyeSlash.d.tsfaEyeSlash.jsfaFastBackward.d.tsfaFastBackward.jsfaFastForward.d.tsfaFastForward.jsfaFax.d.tsfaFax.jsfaFeather.d.tsfaFeather.jsfaFeatherAlt.d.tsfaFeatherAlt.jsfaFemale.d.tsfaFemale.jsfaFighterJet.d.tsfaFighterJet.jsfaFile.d.tsfaFile.jsfaFileAlt.d.tsfaFileAlt.jsfaFileArchive.d.tsfaFileArchive.jsfaFileAudio.d.tsfaFileAudio.jsfaFileCode.d.tsfaFileCode.jsfaFileContract.d.tsfaFileContract.jsfaFileDownload.d.tsfaFileDownload.jsfaFileExcel.d.tsfaFileExcel.jsfaFileExport.d.tsfaFileExport.jsfaFileImage.d.tsfaFileImage.jsfaFileImport.d.tsfaFileImport.jsfaFileInvoice.d.tsfaFileInvoice.jsfaFileInvoiceDollar.d.tsfaFileInvoiceDollar.jsfaFileMedical.d.tsfaFileMedical.jsfaFileMedicalAlt.d.tsfaFileMedicalAlt.jsfaFilePdf.d.tsfaFilePdf.jsfaFilePowerpoint.d.tsfaFilePowerpoint.jsfaFilePrescription.d.tsfaFilePrescription.jsfaFileSignature.d.tsfaFileSignature.jsfaFileUpload.d.tsfaFileUpload.jsfaFileVideo.d.tsfaFileVideo.jsfaFileWord.d.tsfaFileWord.jsfaFill.d.tsfaFill.jsfaFillDrip.d.tsfaFillDrip.jsfaFilm.d.tsfaFilm.jsfaFilter.d.tsfaFilter.jsfaFingerprint.d.tsfaFingerprint.jsfaFire.d.tsfaFire.jsfaFireExtinguisher.d.tsfaFireExtinguisher.jsfaFirstAid.d.tsfaFirstAid.jsfaFish.d.tsfaFish.jsfaFlag.d.tsfaFlag.jsfaFlagCheckered.d.tsfaFlagCheckered.jsfaFlask.d.tsfaFlask.jsfaFlushed.d.tsfaFlushed.jsfaFolder.d.tsfaFolder.jsfaFolderOpen.d.tsfaFolderOpen.jsfaFont.d.tsfaFont.jsfaFontAwesomeLogoFull.d.tsfaFontAwesomeLogoFull.jsfaFootballBall.d.tsfaFootballBall.jsfaForward.d.tsfaForward.jsfaFrog.d.tsfaFrog.jsfaFrown.d.tsfaFrown.jsfaFrownOpen.d.tsfaFrownOpen.jsfaFutbol.d.tsfaFutbol.jsfaGamepad.d.tsfaGamepad.jsfaGasPump.d.tsfaGasPump.jsfaGavel.d.tsfaGavel.jsfaGem.d.tsfaGem.jsfaGenderless.d.tsfaGenderless.jsfaGift.d.tsfaGift.jsfaGlassMartini.d.tsfaGlassMartini.jsfaGlassMartiniAlt.d.tsfaGlassMartiniAlt.jsfaGlasses.d.tsfaGlasses.jsfaGlobe.d.tsfaGlobe.jsfaGlobeAfrica.d.tsfaGlobeAfrica.jsfaGlobeAmericas.d.tsfaGlobeAmericas.jsfaGlobeAsia.d.tsfaGlobeAsia.jsfaGolfBall.d.tsfaGolfBall.jsfaGraduationCap.d.tsfaGraduationCap.jsfaGreaterThan.d.tsfaGreaterThan.jsfaGreaterThanEqual.d.tsfaGreaterThanEqual.jsfaGrimace.d.tsfaGrimace.jsfaGrin.d.tsfaGrin.jsfaGrinAlt.d.tsfaGrinAlt.jsfaGrinBeam.d.tsfaGrinBeam.jsfaGrinBeamSweat.d.tsfaGrinBeamSweat.jsfaGrinHearts.d.tsfaGrinHearts.jsfaGrinSquint.d.tsfaGrinSquint.jsfaGrinSquintTears.d.tsfaGrinSquintTears.jsfaGrinStars.d.tsfaGrinStars.jsfaGrinTears.d.tsfaGrinTears.jsfaGrinTongue.d.tsfaGrinTongue.jsfaGrinTongueSquint.d.tsfaGrinTongueSquint.jsfaGrinTongueWink.d.tsfaGrinTongueWink.jsfaGrinWink.d.tsfaGrinWink.jsfaGripHorizontal.d.tsfaGripHorizontal.jsfaGripVertical.d.tsfaGripVertical.jsfaHSquare.d.tsfaHSquare.jsfaHandHolding.d.tsfaHandHolding.jsfaHandHoldingHeart.d.tsfaHandHoldingHeart.jsfaHandHoldingUsd.d.tsfaHandHoldingUsd.jsfaHandLizard.d.tsfaHandLizard.jsfaHandPaper.d.tsfaHandPaper.jsfaHandPeace.d.tsfaHandPeace.jsfaHandPointDown.d.tsfaHandPointDown.jsfaHandPointLeft.d.tsfaHandPointLeft.jsfaHandPointRight.d.tsfaHandPointRight.jsfaHandPointUp.d.tsfaHandPointUp.jsfaHandPointer.d.tsfaHandPointer.jsfaHandRock.d.tsfaHandRock.jsfaHandScissors.d.tsfaHandScissors.jsfaHandSpock.d.tsfaHandSpock.jsfaHands.d.tsfaHands.jsfaHandsHelping.d.tsfaHandsHelping.jsfaHandshake.d.tsfaHandshake.jsfaHashtag.d.tsfaHashtag.jsfaHdd.d.tsfaHdd.jsfaHeading.d.tsfaHeading.jsfaHeadphones.d.tsfaHeadphones.jsfaHeadphonesAlt.d.tsfaHeadphonesAlt.jsfaHeadset.d.tsfaHeadset.jsfaHeart.d.tsfaHeart.jsfaHeartbeat.d.tsfaHeartbeat.jsfaHelicopter.d.tsfaHelicopter.jsfaHighlighter.d.tsfaHighlighter.jsfaHistory.d.tsfaHistory.jsfaHockeyPuck.d.tsfaHockeyPuck.jsfaHome.d.tsfaHome.jsfaHospital.d.tsfaHospital.jsfaHospitalAlt.d.tsfaHospitalAlt.jsfaHospitalSymbol.d.tsfaHospitalSymbol.jsfaHotTub.d.tsfaHotTub.jsfaHotel.d.tsfaHotel.jsfaHourglass.d.tsfaHourglass.jsfaHourglassEnd.d.tsfaHourglassEnd.jsfaHourglassHalf.d.tsfaHourglassHalf.jsfaHourglassStart.d.tsfaHourglassStart.jsfaICursor.d.tsfaICursor.jsfaIdBadge.d.tsfaIdBadge.jsfaIdCard.d.tsfaIdCard.jsfaIdCardAlt.d.tsfaIdCardAlt.jsfaImage.d.tsfaImage.jsfaImages.d.tsfaImages.jsfaInbox.d.tsfaInbox.jsfaIndent.d.tsfaIndent.jsfaIndustry.d.tsfaIndustry.jsfaInfinity.d.tsfaInfinity.jsfaInfo.d.tsfaInfo.jsfaInfoCircle.d.tsfaInfoCircle.jsfaItalic.d.tsfaItalic.jsfaJoint.d.tsfaJoint.jsfaKey.d.tsfaKey.jsfaKeyboard.d.tsfaKeyboard.jsfaKiss.d.tsfaKiss.jsfaKissBeam.d.tsfaKissBeam.jsfaKissWinkHeart.d.tsfaKissWinkHeart.jsfaKiwiBird.d.tsfaKiwiBird.jsfaLanguage.d.tsfaLanguage.jsfaLaptop.d.tsfaLaptop.jsfaLaugh.d.tsfaLaugh.jsfaLaughBeam.d.tsfaLaughBeam.jsfaLaughSquint.d.tsfaLaughSquint.jsfaLaughWink.d.tsfaLaughWink.jsfaLeaf.d.tsfaLeaf.jsfaLemon.d.tsfaLemon.jsfaLessThan.d.tsfaLessThan.jsfaLessThanEqual.d.tsfaLessThanEqual.jsfaLevelDownAlt.d.tsfaLevelDownAlt.jsfaLevelUpAlt.d.tsfaLevelUpAlt.jsfaLifeRing.d.tsfaLifeRing.jsfaLightbulb.d.tsfaLightbulb.jsfaLink.d.tsfaLink.jsfaLiraSign.d.tsfaLiraSign.jsfaList.d.tsfaList.jsfaListAlt.d.tsfaListAlt.jsfaListOl.d.tsfaListOl.jsfaListUl.d.tsfaListUl.jsfaLocationArrow.d.tsfaLocationArrow.jsfaLock.d.tsfaLock.jsfaLockOpen.d.tsfaLockOpen.jsfaLongArrowAltDown.d.tsfaLongArrowAltDown.jsfaLongArrowAltLeft.d.tsfaLongArrowAltLeft.jsfaLongArrowAltRight.d.tsfaLongArrowAltRight.jsfaLongArrowAltUp.d.tsfaLongArrowAltUp.jsfaLowVision.d.tsfaLowVision.jsfaLuggageCart.d.tsfaLuggageCart.jsfaMagic.d.tsfaMagic.jsfaMagnet.d.tsfaMagnet.jsfaMale.d.tsfaMale.jsfaMap.d.tsfaMap.jsfaMapMarked.d.tsfaMapMarked.jsfaMapMarkedAlt.d.tsfaMapMarkedAlt.jsfaMapMarker.d.tsfaMapMarker.jsfaMapMarkerAlt.d.tsfaMapMarkerAlt.jsfaMapPin.d.tsfaMapPin.jsfaMapSigns.d.tsfaMapSigns.jsfaMarker.d.tsfaMarker.jsfaMars.d.tsfaMars.jsfaMarsDouble.d.tsfaMarsDouble.jsfaMarsStroke.d.tsfaMarsStroke.jsfaMarsStrokeH.d.tsfaMarsStrokeH.jsfaMarsStrokeV.d.tsfaMarsStrokeV.jsfaMedal.d.tsfaMedal.jsfaMedkit.d.tsfaMedkit.jsfaMeh.d.tsfaMeh.jsfaMehBlank.d.tsfaMehBlank.jsfaMehRollingEyes.d.tsfaMehRollingEyes.jsfaMemory.d.tsfaMemory.jsfaMercury.d.tsfaMercury.jsfaMicrochip.d.tsfaMicrochip.jsfaMicrophone.d.tsfaMicrophone.jsfaMicrophoneAlt.d.tsfaMicrophoneAlt.jsfaMicrophoneAltSlash.d.tsfaMicrophoneAltSlash.jsfaMicrophoneSlash.d.tsfaMicrophoneSlash.jsfaMinus.d.tsfaMinus.jsfaMinusCircle.d.tsfaMinusCircle.jsfaMinusSquare.d.tsfaMinusSquare.jsfaMobile.d.tsfaMobile.jsfaMobileAlt.d.tsfaMobileAlt.jsfaMoneyBill.d.tsfaMoneyBill.jsfaMoneyBillAlt.d.tsfaMoneyBillAlt.jsfaMoneyBillWave.d.tsfaMoneyBillWave.jsfaMoneyBillWaveAlt.d.tsfaMoneyBillWaveAlt.jsfaMoneyCheck.d.tsfaMoneyCheck.jsfaMoneyCheckAlt.d.tsfaMoneyCheckAlt.jsfaMonument.d.tsfaMonument.jsfaMoon.d.tsfaMoon.jsfaMortarPestle.d.tsfaMortarPestle.jsfaMotorcycle.d.tsfaMotorcycle.jsfaMousePointer.d.tsfaMousePointer.jsfaMusic.d.tsfaMusic.jsfaNeuter.d.tsfaNeuter.jsfaNewspaper.d.tsfaNewspaper.jsfaNotEqual.d.tsfaNotEqual.jsfaNotesMedical.d.tsfaNotesMedical.jsfaObjectGroup.d.tsfaObjectGroup.jsfaObjectUngroup.d.tsfaObjectUngroup.jsfaOutdent.d.tsfaOutdent.jsfaPaintBrush.d.tsfaPaintBrush.jsfaPaintRoller.d.tsfaPaintRoller.jsfaPalette.d.tsfaPalette.jsfaPallet.d.tsfaPallet.jsfaPaperPlane.d.tsfaPaperPlane.jsfaPaperclip.d.tsfaPaperclip.jsfaParachuteBox.d.tsfaParachuteBox.jsfaParagraph.d.tsfaParagraph.jsfaParking.d.tsfaParking.jsfaPassport.d.tsfaPassport.jsfaPaste.d.tsfaPaste.jsfaPause.d.tsfaPause.jsfaPauseCircle.d.tsfaPauseCircle.jsfaPaw.d.tsfaPaw.jsfaPen.d.tsfaPen.jsfaPenAlt.d.tsfaPenAlt.jsfaPenFancy.d.tsfaPenFancy.jsfaPenNib.d.tsfaPenNib.jsfaPenSquare.d.tsfaPenSquare.jsfaPencilAlt.d.tsfaPencilAlt.jsfaPencilRuler.d.tsfaPencilRuler.jsfaPeopleCarry.d.tsfaPeopleCarry.jsfaPercent.d.tsfaPercent.jsfaPercentage.d.tsfaPercentage.jsfaPhone.d.tsfaPhone.jsfaPhoneSlash.d.tsfaPhoneSlash.jsfaPhoneSquare.d.tsfaPhoneSquare.jsfaPhoneVolume.d.tsfaPhoneVolume.jsfaPiggyBank.d.tsfaPiggyBank.jsfaPills.d.tsfaPills.jsfaPlane.d.tsfaPlane.jsfaPlaneArrival.d.tsfaPlaneArrival.jsfaPlaneDeparture.d.tsfaPlaneDeparture.jsfaPlay.d.tsfaPlay.jsfaPlayCircle.d.tsfaPlayCircle.jsfaPlug.d.tsfaPlug.jsfaPlus.d.tsfaPlus.jsfaPlusCircle.d.tsfaPlusCircle.jsfaPlusSquare.d.tsfaPlusSquare.jsfaPodcast.d.tsfaPodcast.jsfaPoo.d.tsfaPoo.jsfaPortrait.d.tsfaPortrait.jsfaPoundSign.d.tsfaPoundSign.jsfaPowerOff.d.tsfaPowerOff.jsfaPrescription.d.tsfaPrescription.jsfaPrescriptionBottle.d.tsfaPrescriptionBottle.jsfaPrescriptionBottleAlt.d.tsfaPrescriptionBottleAlt.jsfaPrint.d.tsfaPrint.jsfaProcedures.d.tsfaProcedures.jsfaProjectDiagram.d.tsfaProjectDiagram.jsfaPuzzlePiece.d.tsfaPuzzlePiece.jsfaQrcode.d.tsfaQrcode.jsfaQuestion.d.tsfaQuestion.jsfaQuestionCircle.d.tsfaQuestionCircle.jsfaQuidditch.d.tsfaQuidditch.jsfaQuoteLeft.d.tsfaQuoteLeft.jsfaQuoteRight.d.tsfaQuoteRight.jsfaRandom.d.tsfaRandom.jsfaReceipt.d.tsfaReceipt.jsfaRecycle.d.tsfaRecycle.jsfaRedo.d.tsfaRedo.jsfaRedoAlt.d.tsfaRedoAlt.jsfaRegistered.d.tsfaRegistered.jsfaReply.d.tsfaReply.jsfaReplyAll.d.tsfaReplyAll.jsfaRetweet.d.tsfaRetweet.jsfaRibbon.d.tsfaRibbon.jsfaRoad.d.tsfaRoad.jsfaRobot.d.tsfaRobot.jsfaRocket.d.tsfaRocket.jsfaRss.d.tsfaRss.jsfaRssSquare.d.tsfaRssSquare.jsfaRubleSign.d.tsfaRubleSign.jsfaRuler.d.tsfaRuler.jsfaRulerCombined.d.tsfaRulerCombined.jsfaRulerHorizontal.d.tsfaRulerHorizontal.jsfaRulerVertical.d.tsfaRulerVertical.jsfaRupeeSign.d.tsfaRupeeSign.jsfaSadCry.d.tsfaSadCry.jsfaSadTear.d.tsfaSadTear.jsfaSave.d.tsfaSave.jsfaSchool.d.tsfaSchool.jsfaScrewdriver.d.tsfaScrewdriver.jsfaSearch.d.tsfaSearch.jsfaSearchMinus.d.tsfaSearchMinus.jsfaSearchPlus.d.tsfaSearchPlus.jsfaSeedling.d.tsfaSeedling.jsfaServer.d.tsfaServer.jsfaShare.d.tsfaShare.jsfaShareAlt.d.tsfaShareAlt.jsfaShareAltSquare.d.tsfaShareAltSquare.jsfaShareSquare.d.tsfaShareSquare.jsfaShekelSign.d.tsfaShekelSign.jsfaShieldAlt.d.tsfaShieldAlt.jsfaShip.d.tsfaShip.jsfaShippingFast.d.tsfaShippingFast.jsfaShoePrints.d.tsfaShoePrints.jsfaShoppingBag.d.tsfaShoppingBag.jsfaShoppingBasket.d.tsfaShoppingBasket.jsfaShoppingCart.d.tsfaShoppingCart.jsfaShower.d.tsfaShower.jsfaShuttleVan.d.tsfaShuttleVan.jsfaSign.d.tsfaSign.jsfaSignInAlt.d.tsfaSignInAlt.jsfaSignLanguage.d.tsfaSignLanguage.jsfaSignOutAlt.d.tsfaSignOutAlt.jsfaSignal.d.tsfaSignal.jsfaSignature.d.tsfaSignature.jsfaSitemap.d.tsfaSitemap.jsfaSkull.d.tsfaSkull.jsfaSlidersH.d.tsfaSlidersH.jsfaSmile.d.tsfaSmile.jsfaSmileBeam.d.tsfaSmileBeam.jsfaSmileWink.d.tsfaSmileWink.jsfaSmoking.d.tsfaSmoking.jsfaSmokingBan.d.tsfaSmokingBan.jsfaSnowflake.d.tsfaSnowflake.jsfaSolarPanel.d.tsfaSolarPanel.jsfaSort.d.tsfaSort.jsfaSortAlphaDown.d.tsfaSortAlphaDown.jsfaSortAlphaUp.d.tsfaSortAlphaUp.jsfaSortAmountDown.d.tsfaSortAmountDown.jsfaSortAmountUp.d.tsfaSortAmountUp.jsfaSortDown.d.tsfaSortDown.jsfaSortNumericDown.d.tsfaSortNumericDown.jsfaSortNumericUp.d.tsfaSortNumericUp.jsfaSortUp.d.tsfaSortUp.jsfaSpa.d.tsfaSpa.jsfaSpaceShuttle.d.tsfaSpaceShuttle.jsfaSpinner.d.tsfaSpinner.jsfaSplotch.d.tsfaSplotch.jsfaSprayCan.d.tsfaSprayCan.jsfaSquare.d.tsfaSquare.jsfaSquareFull.d.tsfaSquareFull.jsfaStamp.d.tsfaStamp.jsfaStar.d.tsfaStar.jsfaStarHalf.d.tsfaStarHalf.jsfaStarHalfAlt.d.tsfaStarHalfAlt.jsfaStepBackward.d.tsfaStepBackward.jsfaStepForward.d.tsfaStepForward.jsfaStethoscope.d.tsfaStethoscope.jsfaStickyNote.d.tsfaStickyNote.jsfaStop.d.tsfaStop.jsfaStopCircle.d.tsfaStopCircle.jsfaStopwatch.d.tsfaStopwatch.jsfaStore.d.tsfaStore.jsfaStoreAlt.d.tsfaStoreAlt.jsfaStream.d.tsfaStream.jsfaStreetView.d.tsfaStreetView.jsfaStrikethrough.d.tsfaStrikethrough.jsfaStroopwafel.d.tsfaStroopwafel.jsfaSubscript.d.tsfaSubscript.jsfaSubway.d.tsfaSubway.jsfaSuitcase.d.tsfaSuitcase.jsfaSuitcaseRolling.d.tsfaSuitcaseRolling.jsfaSun.d.tsfaSun.jsfaSuperscript.d.tsfaSuperscript.jsfaSurprise.d.tsfaSurprise.jsfaSwatchbook.d.tsfaSwatchbook.jsfaSwimmer.d.tsfaSwimmer.jsfaSwimmingPool.d.tsfaSwimmingPool.jsfaSync.d.tsfaSync.jsfaSyncAlt.d.tsfaSyncAlt.jsfaSyringe.d.tsfaSyringe.jsfaTable.d.tsfaTable.jsfaTableTennis.d.tsfaTableTennis.jsfaTablet.d.tsfaTablet.jsfaTabletAlt.d.tsfaTabletAlt.jsfaTablets.d.tsfaTablets.jsfaTachometerAlt.d.tsfaTachometerAlt.jsfaTag.d.tsfaTag.jsfaTags.d.tsfaTags.jsfaTape.d.tsfaTape.jsfaTasks.d.tsfaTasks.jsfaTaxi.d.tsfaTaxi.jsfaTerminal.d.tsfaTerminal.jsfaTextHeight.d.tsfaTextHeight.jsfaTextWidth.d.tsfaTextWidth.jsfaTh.d.tsfaTh.jsfaThLarge.d.tsfaThLarge.jsfaThList.d.tsfaThList.jsfaThermometer.d.tsfaThermometer.jsfaThermometerEmpty.d.tsfaThermometerEmpty.jsfaThermometerFull.d.tsfaThermometerFull.jsfaThermometerHalf.d.tsfaThermometerHalf.jsfaThermometerQuarter.d.tsfaThermometerQuarter.jsfaThermometerThreeQuarters.d.tsfaThermometerThreeQuarters.jsfaThumbsDown.d.tsfaThumbsDown.jsfaThumbsUp.d.tsfaThumbsUp.jsfaThumbtack.d.tsfaThumbtack.jsfaTicketAlt.d.tsfaTicketAlt.jsfaTimes.d.tsfaTimes.jsfaTimesCircle.d.tsfaTimesCircle.jsfaTint.d.tsfaTint.jsfaTintSlash.d.tsfaTintSlash.jsfaTired.d.tsfaTired.jsfaToggleOff.d.tsfaToggleOff.jsfaToggleOn.d.tsfaToggleOn.jsfaToolbox.d.tsfaToolbox.jsfaTooth.d.tsfaTooth.jsfaTrademark.d.tsfaTrademark.jsfaTrain.d.tsfaTrain.jsfaTransgender.d.tsfaTransgender.jsfaTransgenderAlt.d.tsfaTransgenderAlt.jsfaTrash.d.tsfaTrash.jsfaTrashAlt.d.tsfaTrashAlt.jsfaTree.d.tsfaTree.jsfaTrophy.d.tsfaTrophy.jsfaTruck.d.tsfaTruck.jsfaTruckLoading.d.tsfaTruckLoading.jsfaTruckMoving.d.tsfaTruckMoving.jsfaTshirt.d.tsfaTshirt.jsfaTty.d.tsfaTty.jsfaTv.d.tsfaTv.jsfaUmbrella.d.tsfaUmbrella.jsfaUmbrellaBeach.d.tsfaUmbrellaBeach.jsfaUnderline.d.tsfaUnderline.jsfaUndo.d.tsfaUndo.jsfaUndoAlt.d.tsfaUndoAlt.jsfaUniversalAccess.d.tsfaUniversalAccess.jsfaUniversity.d.tsfaUniversity.jsfaUnlink.d.tsfaUnlink.jsfaUnlock.d.tsfaUnlock.jsfaUnlockAlt.d.tsfaUnlockAlt.jsfaUpload.d.tsfaUpload.jsfaUser.d.tsfaUser.jsfaUserAlt.d.tsfaUserAlt.jsfaUserAltSlash.d.tsfaUserAltSlash.jsfaUserAstronaut.d.tsfaUserAstronaut.jsfaUserCheck.d.tsfaUserCheck.jsfaUserCircle.d.tsfaUserCircle.jsfaUserClock.d.tsfaUserClock.jsfaUserCog.d.tsfaUserCog.jsfaUserEdit.d.tsfaUserEdit.jsfaUserFriends.d.tsfaUserFriends.jsfaUserGraduate.d.tsfaUserGraduate.jsfaUserLock.d.tsfaUserLock.jsfaUserMd.d.tsfaUserMd.jsfaUserMinus.d.tsfaUserMinus.jsfaUserNinja.d.tsfaUserNinja.jsfaUserPlus.d.tsfaUserPlus.jsfaUserSecret.d.tsfaUserSecret.jsfaUserShield.d.tsfaUserShield.jsfaUserSlash.d.tsfaUserSlash.jsfaUserTag.d.tsfaUserTag.jsfaUserTie.d.tsfaUserTie.jsfaUserTimes.d.tsfaUserTimes.jsfaUsers.d.tsfaUsers.jsfaUsersCog.d.tsfaUsersCog.jsfaUtensilSpoon.d.tsfaUtensilSpoon.jsfaUtensils.d.tsfaUtensils.jsfaVectorSquare.d.tsfaVectorSquare.jsfaVenus.d.tsfaVenus.jsfaVenusDouble.d.tsfaVenusDouble.jsfaVenusMars.d.tsfaVenusMars.jsfaVial.d.tsfaVial.jsfaVials.d.tsfaVials.jsfaVideo.d.tsfaVideo.jsfaVideoSlash.d.tsfaVideoSlash.jsfaVolleyballBall.d.tsfaVolleyballBall.jsfaVolumeDown.d.tsfaVolumeDown.jsfaVolumeOff.d.tsfaVolumeOff.jsfaVolumeUp.d.tsfaVolumeUp.jsfaWalking.d.tsfaWalking.jsfaWallet.d.tsfaWallet.jsfaWarehouse.d.tsfaWarehouse.jsfaWeight.d.tsfaWeight.jsfaWeightHanging.d.tsfaWeightHanging.jsfaWheelchair.d.tsfaWheelchair.jsfaWifi.d.tsfaWifi.jsfaWindowClose.d.tsfaWindowClose.jsfaWindowMaximize.d.tsfaWindowMaximize.jsfaWindowMinimize.d.tsfaWindowMinimize.jsfaWindowRestore.d.tsfaWindowRestore.jsfaWineGlass.d.tsfaWineGlass.jsfaWineGlassAlt.d.tsfaWineGlassAlt.jsfaWonSign.d.tsfaWonSign.jsfaWrench.d.tsfaWrench.jsfaXRay.d.tsfaXRay.jsfaYenSign.d.tsfaYenSign.jsindex.d.tsindex.es.jsindex.jspackage.json
svg-with-js
use-on-desktop
web-fonts-with-css
bootstrap
.babelrc.js.browserslistrc.editorconfig.eslintignore.eslintrc.json
.github
.stylelintignore.stylelintrc.travis.ymlCNAMECODE_OF_CONDUCT.mdGemfileGemfile.lockLICENSEREADME.md_config.ymlcomposer.json
dist
js
nuget
package-lock.jsonpackage.jspackage.jsonsache.json
scss
site
_data
_includes
_layouts
docs
4.1
about
assets
browser-bugs.md
components
content
examples
extend
getting-started
layout
migration.md
utilities
versions.html
favicon.icoindex.htmlrobots.txtsw.js
config.toml
i18n
layouts
postcss.config.js
static
vanity.yaml
test

96
.dockerignore Normal file

@ -0,0 +1,96 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# vim swap files
*swp
*swo
*~
# Ping data files
*.ping
*.pings
*.population*
*.percent
*.cities
populations
# Discarded code snippets
build.sh
*-fast.yaml
detritus/
# Dotnet Core ignores
*.swp
*.*~
project.lock.json
.DS_Store
*.pyc
nupkg/
# Visual Studio Code
.vscode
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
msbuild.log
msbuild.err
msbuild.wrn
# Visual Studio 2015
.vs/
# Goland
.idea/
# Nodejs files placed when building Hugo, ok to allow if we actually start using Nodejs.
package.json
package-lock.json
site/resources/_gen/
# Node Modules
node_modules/
# Install YAML files, Helm is the source of truth for configuration.
install/yaml/
# Temp Directories
tmp/
# Compiled Binaries
cmd/minimatch/minimatch
cmd/backendapi/backendapi
cmd/frontendapi/frontendapi
cmd/mmlogicapi/mmlogicapi
examples/backendclient/backendclient
examples/evaluators/golang/serving/serving
examples/functions/golang/grpc-serving/grpc-serving
test/cmd/clientloadgen/clientloadgen
test/cmd/frontendclient/frontendclient
build/

15
.gcloudignore Normal file

@ -0,0 +1,15 @@
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
#!include:.gitignore

31
.gitignore vendored

@ -26,6 +26,8 @@ populations
# Discarded code snippets
build.sh
*-fast.yaml
detritus/
# Dotnet Core ignores
*.swp
@ -62,3 +64,32 @@ msbuild.wrn
# Visual Studio 2015
.vs/
# Goland
.idea/
# Nodejs files placed when building Hugo, ok to allow if we actually start using Nodejs.
package.json
package-lock.json
site/resources/_gen/
# Node Modules
node_modules/
# Install YAML files
install/yaml/
# Temp Directories
tmp/
# Compiled Binaries
cmd/minimatch/minimatch
cmd/backendapi/backendapi
cmd/frontendapi/frontendapi
cmd/mmlogicapi/mmlogicapi
examples/backendclient/backendclient
examples/evaluators/golang/serving/serving
examples/functions/golang/grpc-serving/grpc-serving
test/cmd/clientloadgen/clientloadgen
test/cmd/frontendclient/frontendclient

60
CHANGELOG.md Normal file

@ -0,0 +1,60 @@
# Release history
## v0.4.0 (alpha)
### Release notes
- Thanks to completion of Issues [#42](issues/42) and [#45](issues/45), there is no longer a need to use the `openmatch-base` image when building components of Open Match. Each stand alone appliation now is self-contained in its `Dockerfile` and `cloudbuild.yaml` files, and builds have been substantially simplified. **Note**: The default `Dockerfile` and `cloudbuild.yaml` now tag their images with the version number, not `dev`, and the YAML files in the `install` directory now reflect this.
- This paves the way for CI/CD in an upcoming version.
- This paves the way for public images in an upcoming version!
## v0.3.0 (alpha)
This update is focused on the Frontend API and Player Records, including more robust code for indexing, deindexing, reading, writing, and expiring player requests from Open Match state storage. All Frontend API function argument have changed, although many only slightly. Please join the [Slack channel](https://open-match.slack.com/) if you need help ([Signup link](https://join.slack.com/t/open-match/shared_invite/enQtNDM1NjcxNTY4MTgzLWQzMzE1MGY5YmYyYWY3ZjE2MjNjZTdmYmQ1ZTQzMmNiNGViYmQyN2M4ZmVkMDY2YzZlOTUwMTYwMzI1Y2I2MjU))!
### Release notes
- The Frontend API calls have all be changed to reflect the fact that they operate on Players in state storage. To queue a game client, 'CreatePlayer' in Open Match, to get updates 'GetUpdates', and to stop matching, 'DeletePlayer'. The calls are now much more obviously related to how Open Match sees players: they are database records that it creates on demand, updates using MMFs and the Backend API, and deletes when the player is no longer looking for a match.
- The Player record in state storage has changed to a more complete hash format, and it no longer makes sense to remove a player's assignment from the Frontend as a separate action to removing their record entirely. `DeleteAssignment()` has therefore been removed. Just use `DeletePlayer` instead; you'll always want the client to re-request matching with its latest attributes anyway.
- There is now a module for [indexing and deindexing players in state storage](internal/statestorage/redis/playerindices/playerindices.go). This is a *much* more efficient, as well as being cleaner and more maintainable than the previous implementation which was **hard-coded to index everything** you passed in to the Frontend API at a specific JSON object depth.
- This paves the way for dynamically choosing your indicies without restarting the matchmaker. This will be implemented if there is demand. Pull Requests are welcome!
- Two internal timestamp-based indices have replaced the previous `timestamp` index. `created` is used to calculate how long a player has been waiting for a match, `accessed` is used to determine when a player needs to be expired out of state storage. Both are prefixed by the string `OM_METADATA` so it should be easy to spot them.
- A call to the Frontend API `GetUpdates()` gRPC endpoint returns a stream of player messages. This is used to send updates to state storage for the `Assignment`, `Status`, and `Error` Player fields in near-realtime. **It is the responsibility of the game client to disconnect** from the stream when it has gotten the results it was waiting for!
- Moved the rest of the gRPC messages into a shared [`messages.proto` file](api/protobuf-spec/messages.proto).
- Added documentation to Frontend API gRPC calls to the [`frontend.proto` file](api/protobuf-spec/frontend.proto).
- [Issue #41](https://github.com/GoogleCloudPlatform/open-match/issues/41)|[PR #48](https://github.com/GoogleCloudPlatform/open-match/pull/48) There is now a HA Redis install available in `install/yaml/01-redis-failover.yaml`. This would be used as a drop-in replacement for a single-instance Redis configuration in `install/yaml/01-redis.yaml`. The HA configuration requires that you install the [Redis Operator](https://github.com/spotahome/redis-operator) (note: **currently alpha**, use at your own risk) in your Kubernetes cluster.
- As part of this change, the kubernetes service name is now `redis` not `redis-sentinel` to denote that it is accessed using a standard Redis client.
- Open Match uses a new feature of the go module [logrus](github.com/sirupsen/logrus) to include filenames and line numbers. If you have an older version in your local build environment, you may need to delete the module and `go get github.com/sirupsen/logrus` again. When building using the provided `cloudbuild.yaml` and `Dockerfile`s this is handled for you.
- The program that was formerly in `examples/frontendclient` has been expanded and has been moved to the `test` directory under (`test/cmd/frontendclient/`)[test/cmd/frontendclient/].
- The client load generator program has been moved from `test/cmd/client` to (`test/cmd/clientloadgen/`)[test/cmd/clientloadgen/] to better reflect what it does.
- [Issue #45](https://github.com/GoogleCloudPlatform/open-match/issues/45) The process for moving the build files (`Dockerfile` and `cloudbuild.yaml`) for each component, example, and test program to their respective directories and out of the repository root has started but won't be completed until a future version.
- Put some basic notes in the [production guide](docs/production.md)
- Added a basic [roadmap](docs/roadmap.md)
## v0.2.0 (alpha)
This is a pretty large update. Custom MMFs or evaluators from 0.1.0 may need some tweaking to work with this version. Some Backend API function arguments have changed. Please join the [Slack channel](https://open-match.slack.com/) if you need help ([Signup link](https://join.slack.com/t/open-match/shared_invite/enQtNDM1NjcxNTY4MTgzLWQzMzE1MGY5YmYyYWY3ZjE2MjNjZTdmYmQ1ZTQzMmNiNGViYmQyN2M4ZmVkMDY2YzZlOTUwMTYwMzI1Y2I2MjU))!
v0.2.0 focused on adding additional functionality to Backend API calls and on **reducing the amount of boilerplate code required to make a custom Matchmaking Function**. For this, a new internal API for use by MMFs called the [Matchmaking Logic API (MMLogic API)](README.md#matchmaking-logic-mmlogic-api) has been added. Many of the core components and examples had to be updated to use the new Backend API arguments and the modules to support them, so we recommend you rebuild and redeploy all the components to use v0.2.0.
### Release notes
- MMLogic API is now available. Deploy it to kubernetes using the [appropriate json file]() and check out the [gRPC API specification](api/protobuf-spec/mmlogic.proto) to see how to use it. To write a client against this API, you'll need to compile the protobuf files to your language of choice. There is an associated cloudbuild.yaml file and Dockerfile for it in the root directory.
- When using the MMLogic API to filter players into pools, it will attempt to report back the number of players that matched the filters and how long the filters took to query state storage.
- An [example MMF](examples/functions/python3/mmlogic-simple/harness.py) using it has been written in Python3. There is an associated cloudbuild.yaml file and Dockerfile for it in the root directory. By default the [example backend client](examples/backendclient/main.go) is now configured to use this MMF, so make sure you have it avaiable before you try to run the latest backend client.
- An [example MMF](examples/functions/php/mmlogic-simple/harness.py) using it has been contributed by Ilya Hrankouski in PHP (thanks!). - The API specs have been split into separate files per API and the protobuf messages are in a separate file. Things were renamed slightly as a result, and you will need to update your API clients. The Frontend API hasn't had it's messages moved to the shared messages file yet, but this will happen in an upcoming version.
- The [example golang MMF](examples/functions/golang/manual-simple/) has been updated to use the latest data schemas for MatchObjects, and renamed to `manual-simple` to denote that it is manually manipulating Redis, not using the MMLogic API.
- The API specs have been split into separate files per API and the protobuf messages are in a separate file. Things were renamed slightly as a result, and you will need to update your API clients. The Frontend API hasn't had it's messages moved to the shared messages file yet, but this will happen in an upcoming version.
- The message model for using the Backend API has changed slightly - for calls that make MatchObjects, the expectation is that you will provide a MatchObject with a few fields populated, and it will then be shuttled along through state storage to your MMF and back out again, with various processes 'filling in the blanks' of your MatchObject, which is then returned to your code calling the Backend API. Read the[gRPC API specification](api/protobuf-spec/backend.proto) for more information.
- As part of this, compiled protobuf golang modules now live in the [`internal/pb`](internal/pb) directory. There's a handy [bash script](api/protoc-go.sh) for compiling them from the `api/protobuf-spec` directory into this new `internal/pb` directory for development in your local golang environment if you need it.
- As part of this Backend API message shift and the advent of the MMLogic API, 'player pools' and 'rosters' are now first-class data structures in MatchObjects for those who wish to use them. You can ignore them if you like, but if you want to use some of the MMLogic API calls to automate tasks for you - things like filtering a pool of players according attributes or adding all the players in your rosters to the ignorelist so other MMFs don't try to grab them - you'll need to put your data into the [protobuf messages](api/protobuf-spec/messages.proto) so Open Match knows how to read them. The sample backend client [test profile JSON](examples/backendclient/profiles/testprofile.json)has been updated to use this format if you want to see an example.
- Rosters were formerly space-delimited lists of player IDs. They are now first-class repeated protobuf message fields in the [Roster message format](api/protobuf-spec/messages.proto). That means that in most languages, you can access the roster as a list of players using your native language data structures (more info can be found in the [guide for using protocol buffers in your langauge of choice](https://developers.google.com/protocol-buffers/docs/reference/overview)). If you don't care about the new fields or the new functionality, you can just leave all the other fields but the player ID unset.
- Open Match is transitioning to using [protocol buffer messages](https://developers.google.com/protocol-buffers/) as its internal data format. There is now a Redis state storage [golang module](internal/statestorage/redis/redispb/) for marshaling and unmarshaling MatchObject messages to and from Redis. It isn't very clean code right now but will get worked on for the next couple releases.
- Ignorelists now exist, and have a Redis state storage [golang module](internal/statestorage/redis/ignorelist/) for CRUD access. Currently three ignorelists are defined in the [config file](config/matchmaker_config.json) with their respective parameters. These are implemented as [Sorted Sets in Redis](https://redis.io/commands#sorted_set).
- For those who only want to stand up Open Match and aren't interested in individually tweaking the required kubernetes resources, there are now [three YAML files](install/yaml) that can be used to install Redis, install Open Match, and (optionally) install Prometheus. You'll still need the `sed` [instructions from the Developer Guide](docs/development.md#running-open-match-in-a-development-environment) to substitute in the name of your Docker container registry.
- A super-simple module has been created for doing instersections, unions, and differences of lists of player IDs. It lives in `internal/set/set.go`.
### Roadmap
- It has become clear from talking to multiple users that the software they write to talk to the Backend API needs a name. 'Backend API Client' is technically correct, but given how many APIs are in Open Match and the overwhelming use of 'Client' to refer to a Game Client in the industry, we're currently calling this a 'Director', as its primary purpose is to 'direct' which profiles are sent to the backend, and 'direct' the resulting MatchObjects to game servers. Further discussion / suggestions are welcome.
- We'll be entering the design stage on longer-running MMFs before the end of the year. We'll get a proposal together and on the github repo as a request for comments, so please keep your eye out for that.
- Match profiles providing multiple MMFs to run isn't planned anymore. Just send multiple copies of the profile with different MMFs specified via the backendapi.
- Redis Sentinel will likely not be supported. Instead, replicated instances and HAProxy may be the HA solution of choice. There's an [outstanding issue to investigate and implement](https://github.com/GoogleCloudPlatform/open-match/issues/41) if it fills our needs, feel free to contribute!
## v0.1.0 (alpha)
Initial release.

@ -1,13 +0,0 @@
# Golang application builder steps
FROM golang:1.10.3 as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match
COPY cmd/backendapi cmd/backendapi
COPY config config
COPY internal internal
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/backendapi
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
#FROM scratch
#COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/backendapi/backendapi .
ENTRYPOINT ["./backendapi"]

@ -1,7 +0,0 @@
# Golang application builder steps
FROM golang:1.10.3 as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match
COPY config config
COPY internal internal
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/internal
RUN go get -d -v ...

21
Dockerfile.base-build Normal file

@ -0,0 +1,21 @@
# Copyright 2019 Google LLC
#
# 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.
FROM golang:latest
ENV GO111MODULE=on
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match
COPY . .
RUN go mod download

55
Dockerfile.ci Normal file

@ -0,0 +1,55 @@
# Copyright 2019 Google LLC
#
# 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.
FROM debian
RUN apt-get update
RUN apt-get install -y -qq git make python3 virtualenv curl sudo unzip apt-transport-https ca-certificates curl software-properties-common gnupg2
# Docker
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
RUN sudo apt-key fingerprint 0EBFCD88
RUN sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/debian \
stretch \
stable"
RUN sudo apt-get update
RUN sudo apt-get install -y -qq docker-ce docker-ce-cli containerd.io
# Cloud SDK
RUN export CLOUD_SDK_REPO="cloud-sdk-stretch" && \
echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && \
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \
apt-get update -y && apt-get install google-cloud-sdk -y -qq
# Install Golang
# https://github.com/docker-library/golang/blob/fd272b2b72db82a0bd516ce3d09bba624651516c/1.12/stretch/Dockerfile
RUN mkdir -p /toolchain/golang
WORKDIR /toolchain/golang
RUN sudo rm -rf /usr/local/go/
RUN curl -L https://storage.googleapis.com/golang/go1.12.1.linux-amd64.tar.gz | sudo tar -C /usr/local -xz
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
RUN sudo mkdir -p "$GOPATH/src" "$GOPATH/bin" \
&& sudo chmod -R 777 "$GOPATH"
# Prepare toolchain and workspace
RUN mkdir -p /toolchain
RUN mkdir -p /workspace
WORKDIR /workspace
ENV ALLOW_BUILD_WITH_SUDO=1

@ -1,13 +0,0 @@
# Golang application builder steps
FROM golang:1.10.3 as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match
COPY examples/evaluators/golang/simple examples/evaluators/golang/simple
COPY config config
COPY internal internal
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/examples/evaluators/golang/simple
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
#FROM scratch
#COPY --from=builder /go/src/github.com/GoogleCloudPlatform/mmfstub/mmfstub mmfstub
ENTRYPOINT ["./simple"]

@ -1,13 +0,0 @@
# Golang application builder steps
FROM golang:1.10.3 as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match
COPY cmd/frontendapi cmd/frontendapi
COPY config config
COPY internal internal
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/frontendapi
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
#FROM scratch
#COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/frontendapi/frontendapi .
ENTRYPOINT ["./frontendapi"]

@ -1,12 +0,0 @@
# Golang application builder steps
# FROM golang:1.10.3 as builder
FROM gcr.io/matchmaker-dev-201405/openmatch-devbase as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match
COPY examples/functions/golang/manual-simple examples/functions/golang/manual-simple
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/examples/functions/golang/manual-simple
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o mmf .
#FROM scratch
#COPY --from=builder /go/src/github.com/GoogleCloudPlatform/mmfstub/mmfstub mmfstub
CMD ["./mmf"]

@ -1,21 +0,0 @@
FROM php:7.2-cli
RUN apt-get update && apt-get install -y -q zip unzip zlib1g-dev && apt-get clean
RUN cd /usr/local/bin && curl -sS https://getcomposer.org/installer | php
RUN cd /usr/local/bin && mv composer.phar composer
RUN pecl install grpc
RUN echo "extension=grpc.so" > /usr/local/etc/php/conf.d/30-grpc.ini
RUN pecl install protobuf
RUN echo "extension=protobuf.so" > /usr/local/etc/php/conf.d/30-protobuf.ini
WORKDIR /usr/src/open-match
COPY examples/functions/php/mmlogic-simple examples/functions/php/mmlogic-simple
COPY config config
WORKDIR /usr/src/open-match/examples/functions/php/mmlogic-simple
RUN composer install
CMD [ "php", "./harness.php" ]

@ -1,9 +0,0 @@
# Golang application builder steps
FROM python:3.5.3 as builder
WORKDIR /usr/src/open-match
COPY examples/functions/python3/mmlogic-simple examples/functions/python3/mmlogic-simple
COPY config config
WORKDIR /usr/src/open-match/examples/functions/python3/mmlogic-simple
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "./harness.py"]

@ -1,24 +0,0 @@
# Golang application builder steps
FROM golang:1.10.3 as builder
# Necessary to get a specific version of the golang k8s client
RUN go get github.com/tools/godep
RUN go get k8s.io/client-go/...
WORKDIR /go/src/k8s.io/client-go
RUN git checkout v7.0.0
RUN godep restore ./...
RUN rm -rf vendor/
RUN rm -rf /go/src/github.com/golang/protobuf/
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/
COPY cmd/mmforc cmd/mmforc
COPY config config
COPY internal internal
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/mmforc/
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
# Uncomment to build production images (removes all troubleshooting tools)
#FROM scratch
#COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/mmforc/mmforc .
CMD ["./mmforc"]

@ -1,13 +0,0 @@
# Golang application builder steps
FROM golang:1.10.3 as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match
COPY cmd/mmlogicapi cmd/mmlogicapi
COPY config config
COPY internal internal
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/mmlogicapi
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
#FROM scratch
#COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/frontendapi/frontendapi .
ENTRYPOINT ["./mmlogicapi"]

728
Makefile Normal file

@ -0,0 +1,728 @@
################################################################################
# Open Match Makefile #
################################################################################
# Copyright 2019 Google LLC
#
# 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.
## NOTICE: There's 2 variables you need to make sure are set.
## GCP_PROJECT_ID if you're working against GCP.
## Or $REGISTRY if you want to use your own custom docker registry.
##
## Basic Deployment
## make create-gke-cluster OR make create-mini-cluster
## make push-helm
## make REGISTRY=gcr.io/$PROJECT_ID push-images -j$(nproc)
## make install-chart
## Generate Files
## make all-protos
##
## Building
## make all -j$(nproc)
##
## Access monitoring
## make proxy-prometheus
## make proxy-grafana
##
## Run those tools
## make run-backendclient
## make run-frontendclient
## make run-clientloadgen
##
## Teardown
## make delete-mini-cluster
## make delete-gke-cluster
##
# http://makefiletutorial.com/
BASE_VERSION = 0.5.0-rc1
VERSION_SUFFIX = $(shell git rev-parse --short=7 HEAD | tr -d [:punct:])
BRANCH_NAME = $(shell git rev-parse --abbrev-ref HEAD | tr -d [:punct:])
VERSION = $(BASE_VERSION)-$(VERSION_SUFFIX)
PROTOC_VERSION = 3.7.1
HELM_VERSION = 2.13.1
HUGO_VERSION = 0.55.2
KUBECTL_VERSION = 1.14.1
NODEJS_VERSION = 10.15.3
SKAFFOLD_VERSION = latest
MINIKUBE_VERSION = latest
HTMLTEST_VERSION = 0.10.1
GOLANGCI_VERSION = 1.16.0
PROTOC_RELEASE_BASE = https://github.com/protocolbuffers/protobuf/releases/download/v$(PROTOC_VERSION)/protoc-$(PROTOC_VERSION)
GO = GO111MODULE=on go
# Defines the absolute local directory of the open-match project
REPOSITORY_ROOT := $(realpath $(dir $(abspath $(MAKEFILE_LIST))))
GO_BUILD_COMMAND = CGO_ENABLED=0 $(GO) build -a -installsuffix cgo .
BUILD_DIR = $(REPOSITORY_ROOT)/build
TOOLCHAIN_DIR = $(BUILD_DIR)/toolchain
TOOLCHAIN_BIN = $(TOOLCHAIN_DIR)/bin
PROTOC := $(TOOLCHAIN_BIN)/protoc
PROTOC_INCLUDES := $(TOOLCHAIN_DIR)/include/
GCP_PROJECT_ID ?=
GCP_PROJECT_FLAG = --project=$(GCP_PROJECT_ID)
OPEN_MATCH_PUBLIC_IMAGES_PROJECT_ID = open-match-public-images
OM_SITE_GCP_PROJECT_ID = open-match-site
OM_SITE_GCP_PROJECT_FLAG = --project=$(OM_SITE_GCP_PROJECT_ID)
REGISTRY ?= gcr.io/$(GCP_PROJECT_ID)
TAG := $(VERSION)
ALTERNATE_TAG := dev
GKE_CLUSTER_NAME = om-cluster
GCP_REGION = us-west1
GCP_ZONE = us-west1-a
EXE_EXTENSION =
LOCAL_CLOUD_BUILD_PUSH = # --push
KUBECTL_RUN_ENV = --env='REDIS_SERVICE_HOST=$$(OM_REDIS_MASTER_SERVICE_HOST)' --env='REDIS_SERVICE_PORT=$$(OM_REDIS_MASTER_SERVICE_PORT)'
GCP_LOCATION_FLAG = --zone $(GCP_ZONE)
# Flags to simulate behavior of newer versions of Kubernetes
KUBERNETES_COMPAT = --no-enable-basic-auth --no-issue-client-certificate --enable-ip-alias --metadata disable-legacy-endpoints=true --enable-autoupgrade
GO111MODULE = on
PROMETHEUS_PORT = 9090
GRAFANA_PORT = 3000
SITE_PORT = 8080
HELM = $(TOOLCHAIN_BIN)/helm
TILLER = $(TOOLCHAIN_BIN)/tiller
MINIKUBE = $(TOOLCHAIN_BIN)/minikube
KUBECTL = $(TOOLCHAIN_BIN)/kubectl
HTMLTEST = $(TOOLCHAIN_BIN)/htmltest
SERVICE = default
OPEN_MATCH_CHART_NAME = open-match
OPEN_MATCH_KUBERNETES_NAMESPACE = open-match
OPEN_MATCH_EXAMPLE_CHART_NAME = open-match-example
OPEN_MATCH_EXAMPLE_KUBERNETES_NAMESPACE = open-match
REDIS_NAME = om-redis
GCLOUD_ACCOUNT_EMAIL = $(shell gcloud auth list --format yaml | grep account: | cut -c 10-)
_GCB_POST_SUBMIT ?= 0
# Make port forwards accessible outside of the proxy machine.
PORT_FORWARD_ADDRESS_FLAG = --address 0.0.0.0
DASHBOARD_PORT = 9092
export PATH := $(REPOSITORY_ROOT)/node_modules/.bin/:$(TOOLCHAIN_BIN):$(TOOLCHAIN_DIR)/nodejs/bin:$(PATH)
# Get the project from gcloud if it's not set.
ifeq ($(GCP_PROJECT_ID),)
export GCP_PROJECT_ID = $(shell gcloud config list --format 'value(core.project)')
endif
ifeq ($(OS),Windows_NT)
# TODO: Windows packages are here but things are broken since many paths are Linux based and zip vs tar.gz.
HELM_PACKAGE = https://storage.googleapis.com/kubernetes-helm/helm-v$(HELM_VERSION)-windows-amd64.zip
MINIKUBE_PACKAGE = https://storage.googleapis.com/minikube/releases/$(MINIKUBE_VERSION)/minikube-windows-amd64.exe
SKAFFOLD_PACKAGE = https://storage.googleapis.com/skaffold/releases/$(SKAFFOLD_VERSION)/skaffold-windows-amd64.exe
EXE_EXTENSION = .exe
PROTOC_PACKAGE = $(PROTOC_RELEASE_BASE)-win64.zip
KUBECTL_PACKAGE = https://storage.googleapis.com/kubernetes-release/release/v$(KUBECTL_VERSION)/bin/windows/amd64/kubectl.exe
HUGO_PACKAGE = https://github.com/gohugoio/hugo/releases/download/v$(HUGO_VERSION)/hugo_extended_$(HUGO_VERSION)_Windows-64bit.zip
NODEJS_PACKAGE = https://nodejs.org/dist/v$(NODEJS_VERSION)/node-v$(NODEJS_VERSION)-win-x64.zip
NODEJS_PACKAGE_NAME = nodejs.zip
HTMLTEST_PACKAGE = https://github.com/wjdp/htmltest/releases/download/v$(HTMLTEST_VERSION)/htmltest_$(HTMLTEST_VERSION)_windows_amd64.zip
GOLANGCI_PACKAGE = https://github.com/golangci/golangci-lint/releases/download/v$(GOLANGCI_VERSION)/golangci-lint-$(GOLANGCI_VERSION)-windows-amd64.zip
else
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
HELM_PACKAGE = https://storage.googleapis.com/kubernetes-helm/helm-v$(HELM_VERSION)-linux-amd64.tar.gz
MINIKUBE_PACKAGE = https://storage.googleapis.com/minikube/releases/$(MINIKUBE_VERSION)/minikube-linux-amd64
SKAFFOLD_PACKAGE = https://storage.googleapis.com/skaffold/releases/$(SKAFFOLD_VERSION)/skaffold-linux-amd64
PROTOC_PACKAGE = $(PROTOC_RELEASE_BASE)-linux-x86_64.zip
KUBECTL_PACKAGE = https://storage.googleapis.com/kubernetes-release/release/v$(KUBECTL_VERSION)/bin/linux/amd64/kubectl
HUGO_PACKAGE = https://github.com/gohugoio/hugo/releases/download/v$(HUGO_VERSION)/hugo_extended_$(HUGO_VERSION)_Linux-64bit.tar.gz
NODEJS_PACKAGE = https://nodejs.org/dist/v$(NODEJS_VERSION)/node-v$(NODEJS_VERSION)-linux-x64.tar.gz
NODEJS_PACKAGE_NAME = nodejs.tar.gz
HTMLTEST_PACKAGE = https://github.com/wjdp/htmltest/releases/download/v$(HTMLTEST_VERSION)/htmltest_$(HTMLTEST_VERSION)_linux_amd64.tar.gz
GOLANGCI_PACKAGE = https://github.com/golangci/golangci-lint/releases/download/v$(GOLANGCI_VERSION)/golangci-lint-$(GOLANGCI_VERSION)-linux-amd64.tar.gz
endif
ifeq ($(UNAME_S),Darwin)
HELM_PACKAGE = https://storage.googleapis.com/kubernetes-helm/helm-v$(HELM_VERSION)-darwin-amd64.tar.gz
MINIKUBE_PACKAGE = https://storage.googleapis.com/minikube/releases/$(MINIKUBE_VERSION)/minikube-darwin-amd64
SKAFFOLD_PACKAGE = https://storage.googleapis.com/skaffold/releases/$(SKAFFOLD_VERSION)/skaffold-darwin-amd64
PROTOC_PACKAGE = $(PROTOC_RELEASE_BASE)-osx-x86_64.zip
KUBECTL_PACKAGE = https://storage.googleapis.com/kubernetes-release/release/v$(KUBECTL_VERSION)/bin/darwin/amd64/kubectl
HUGO_PACKAGE = https://github.com/gohugoio/hugo/releases/download/v$(HUGO_VERSION)/hugo_extended_$(HUGO_VERSION)_macOS-64bit.tar.gz
NODEJS_PACKAGE = https://nodejs.org/dist/v$(NODEJS_VERSION)/node-v$(NODEJS_VERSION)-darwin-x64.tar.gz
NODEJS_PACKAGE_NAME = nodejs.tar.gz
HTMLTEST_PACKAGE = https://github.com/wjdp/htmltest/releases/download/v$(HTMLTEST_VERSION)/htmltest_$(HTMLTEST_VERSION)_osx_amd64.tar.gz
GOLANGCI_PACKAGE = https://github.com/golangci/golangci-lint/releases/download/v$(GOLANGCI_VERSION)/golangci-lint-$(GOLANGCI_VERSION)-darwin-amd64.tar.gz
endif
endif
help:
@cat Makefile | grep ^\#\# | grep -v ^\#\#\# |cut -c 4-
local-cloud-build: gcloud
cloud-build-local --config=cloudbuild.yaml --dryrun=false $(LOCAL_CLOUD_BUILD_PUSH) --substitutions SHORT_SHA=$(VERSION_SUFFIX),_GCB_POST_SUBMIT=$(_GCB_POST_SUBMIT),BRANCH_NAME=$(BRANCH_NAME) .
push-images: push-service-images push-client-images push-mmf-example-images push-evaluator-example-images
push-service-images: push-minimatch-image push-frontendapi-image push-backendapi-image push-mmlogicapi-image
push-mmf-example-images: push-mmf-go-grpc-serving-simple-image
push-client-images: push-backendclient-image push-clientloadgen-image push-frontendclient-image
push-evaluator-example-images: push-evaluator-serving-image
push-minimatch-image: docker build-minimatch-image
docker push $(REGISTRY)/openmatch-minimatch:$(TAG)
docker push $(REGISTRY)/openmatch-minimatch:$(ALTERNATE_TAG)
push-frontendapi-image: docker build-frontendapi-image
docker push $(REGISTRY)/openmatch-frontendapi:$(TAG)
docker push $(REGISTRY)/openmatch-frontendapi:$(ALTERNATE_TAG)
push-backendapi-image: docker build-backendapi-image
docker push $(REGISTRY)/openmatch-backendapi:$(TAG)
docker push $(REGISTRY)/openmatch-backendapi:$(ALTERNATE_TAG)
push-mmlogicapi-image: docker build-mmlogicapi-image
docker push $(REGISTRY)/openmatch-mmlogicapi:$(TAG)
docker push $(REGISTRY)/openmatch-mmlogicapi:$(ALTERNATE_TAG)
push-mmf-go-grpc-serving-simple-image: docker build-mmf-go-grpc-serving-simple-image
docker push $(REGISTRY)/openmatch-mmf-go-grpc-serving-simple:$(TAG)
docker push $(REGISTRY)/openmatch-mmf-go-grpc-serving-simple:$(ALTERNATE_TAG)
push-backendclient-image: docker build-backendclient-image
docker push $(REGISTRY)/openmatch-backendclient:$(TAG)
docker push $(REGISTRY)/openmatch-backendclient:$(ALTERNATE_TAG)
push-clientloadgen-image: docker build-clientloadgen-image
docker push $(REGISTRY)/openmatch-clientloadgen:$(TAG)
docker push $(REGISTRY)/openmatch-clientloadgen:$(ALTERNATE_TAG)
push-frontendclient-image: docker build-frontendclient-image
docker push $(REGISTRY)/openmatch-frontendclient:$(TAG)
docker push $(REGISTRY)/openmatch-frontendclient:$(ALTERNATE_TAG)
push-evaluator-serving-image: build-evaluator-serving-image
docker push $(REGISTRY)/openmatch-evaluator-serving:$(TAG)
docker push $(REGISTRY)/openmatch-evaluator-serving:$(ALTERNATE_TAG)
build-images: build-service-images build-client-images build-mmf-example-images build-evaluator-example-images
build-service-images: build-minimatch-image build-frontendapi-image build-backendapi-image build-mmlogicapi-image
build-client-images: build-backendclient-image build-clientloadgen-image build-frontendclient-image
build-mmf-example-images: build-mmf-go-grpc-serving-simple-image
build-evaluator-example-images: build-evaluator-serving-image
build-base-build-image: docker
docker build -f Dockerfile.base-build -t open-match-base-build .
build-minimatch-image: docker build-base-build-image
docker build -f cmd/minimatch/Dockerfile -t $(REGISTRY)/openmatch-minimatch:$(TAG) -t $(REGISTRY)/openmatch-minimatch:$(ALTERNATE_TAG) .
build-frontendapi-image: docker build-base-build-image
docker build -f cmd/frontendapi/Dockerfile -t $(REGISTRY)/openmatch-frontendapi:$(TAG) -t $(REGISTRY)/openmatch-frontendapi:$(ALTERNATE_TAG) .
build-backendapi-image: docker build-base-build-image
docker build -f cmd/backendapi/Dockerfile -t $(REGISTRY)/openmatch-backendapi:$(TAG) -t $(REGISTRY)/openmatch-backendapi:$(ALTERNATE_TAG) .
build-mmlogicapi-image: docker build-base-build-image
docker build -f cmd/mmlogicapi/Dockerfile -t $(REGISTRY)/openmatch-mmlogicapi:$(TAG) -t $(REGISTRY)/openmatch-mmlogicapi:$(ALTERNATE_TAG) .
build-mmf-go-grpc-serving-simple-image: docker build-base-build-image
docker build -f examples/functions/golang/grpc-serving/Dockerfile -t $(REGISTRY)/openmatch-mmf-go-grpc-serving-simple:$(TAG) -t $(REGISTRY)/openmatch-mmf-go-grpc-serving-simple:$(ALTERNATE_TAG) .
build-backendclient-image: docker build-base-build-image
docker build -f examples/backendclient/Dockerfile -t $(REGISTRY)/openmatch-backendclient:$(TAG) -t $(REGISTRY)/openmatch-backendclient:$(ALTERNATE_TAG) .
build-clientloadgen-image: docker build-base-build-image
docker build -f test/cmd/clientloadgen/Dockerfile -t $(REGISTRY)/openmatch-clientloadgen:$(TAG) -t $(REGISTRY)/openmatch-clientloadgen:$(ALTERNATE_TAG) .
build-frontendclient-image: docker build-base-build-image
docker build -f test/cmd/frontendclient/Dockerfile -t $(REGISTRY)/openmatch-frontendclient:$(TAG) -t $(REGISTRY)/openmatch-frontendclient:$(ALTERNATE_TAG) .
build-evaluator-serving-image: build-base-build-image
docker build -f examples/evaluators/golang/serving/Dockerfile -t $(REGISTRY)/openmatch-evaluator-serving:$(TAG) -t $(REGISTRY)/openmatch-evaluator-serving:$(ALTERNATE_TAG) .
clean-images: docker
-docker rmi -f open-match-base-build
-docker rmi -f $(REGISTRY)/openmatch-minimatch:$(TAG) $(REGISTRY)/openmatch-minimatch:$(ALTERNATE_TAG)
-docker rmi -f $(REGISTRY)/openmatch-frontendapi:$(TAG) $(REGISTRY)/openmatch-frontendapi:$(ALTERNATE_TAG)
-docker rmi -f $(REGISTRY)/openmatch-backendapi:$(TAG) $(REGISTRY)/openmatch-backendapi:$(ALTERNATE_TAG)
-docker rmi -f $(REGISTRY)/openmatch-mmlogicapi:$(TAG) $(REGISTRY)/openmatch-mmlogicapi:$(ALTERNATE_TAG)
-docker rmi -f $(REGISTRY)/openmatch-mmf-go-grpc-serving-simple:$(TAG) $(REGISTRY)/openmatch-mmf-go-grpc-serving-simple:$(ALTERNATE_TAG)
-docker rmi -f $(REGISTRY)/openmatch-backendclient:$(TAG) $(REGISTRY)/openmatch-backendclient:$(ALTERNATE_TAG)
-docker rmi -f $(REGISTRY)/openmatch-clientloadgen:$(TAG) $(REGISTRY)/openmatch-clientloadgen:$(ALTERNATE_TAG)
-docker rmi -f $(REGISTRY)/openmatch-frontendclient:$(TAG) $(REGISTRY)/openmatch-frontendclient:$(ALTERNATE_TAG)
-docker rmi -f $(REGISTRY)/openmatch-evaluator-serving:$(TAG) $(REGISTRY)/openmatch-evaluator-serving:$(ALTERNATE_TAG)
install-redis: build/toolchain/bin/helm$(EXE_EXTENSION)
$(HELM) upgrade --install --wait --debug $(REDIS_NAME) stable/redis --namespace $(OPEN_MATCH_KUBERNETES_NAMESPACE)
chart-deps: build/toolchain/bin/helm$(EXE_EXTENSION)
(cd install/helm/open-match; $(HELM) dependency update)
lint-chart: build/toolchain/bin/helm$(EXE_EXTENSION)
(cd install/helm; $(HELM) lint open-match; $(HELM) lint open-match-example)
print-chart: build/toolchain/bin/helm$(EXE_EXTENSION)
(cd install/helm; $(HELM) install --dry-run --debug open-match; $(HELM) install --dry-run --debug open-match-example)
install-chart: build/toolchain/bin/helm$(EXE_EXTENSION)
$(HELM) upgrade --install --wait --debug $(OPEN_MATCH_CHART_NAME) install/helm/open-match \
--namespace=$(OPEN_MATCH_KUBERNETES_NAMESPACE) \
--set openmatch.image.registry=$(REGISTRY) \
--set openmatch.image.tag=$(TAG)
install-example-chart: build/toolchain/bin/helm$(EXE_EXTENSION)
$(HELM) upgrade --install --wait --debug $(OPEN_MATCH_EXAMPLE_CHART_NAME) install/helm/open-match-example \
--namespace=$(OPEN_MATCH_KUBERNETES_NAMESPACE) \
--set openmatch.image.registry=$(REGISTRY) \
--set openmatch.image.tag=$(TAG)
delete-example-chart: build/toolchain/bin/helm$(EXE_EXTENSION)
-$(HELM) delete --purge $(OPEN_MATCH_EXAMPLE_CHART_NAME)
dry-chart: build/toolchain/bin/helm$(EXE_EXTENSION)
$(HELM) upgrade --install --wait --debug --dry-run $(OPEN_MATCH_CHART_NAME) install/helm/open-match \
--namespace=$(OPEN_MATCH_KUBERNETES_NAMESPACE) \
--set openmatch.image.registry=$(REGISTRY) \
--set openmatch.image.tag=$(TAG)
delete-chart: build/toolchain/bin/helm$(EXE_EXTENSION) build/toolchain/bin/kubectl$(EXE_EXTENSION)
-$(HELM) delete --purge $(OPEN_MATCH_CHART_NAME)
-$(KUBECTL) delete crd prometheuses.monitoring.coreos.com
-$(KUBECTL) delete crd servicemonitors.monitoring.coreos.com
-$(KUBECTL) delete crd prometheusrules.monitoring.coreos.com
update-helm-deps: build/toolchain/bin/helm$(EXE_EXTENSION)
(cd install/helm/open-match; $(HELM) dependencies update)
install/yaml/: install/yaml/install.yaml install/yaml/install-example.yaml install/yaml/01-redis-chart.yaml install/yaml/02-open-match.yaml install/yaml/03-prometheus-chart.yaml install/yaml/04-grafana-chart.yaml
install/yaml/01-redis-chart.yaml: build/toolchain/bin/helm$(EXE_EXTENSION)
mkdir -p install/yaml/
$(HELM) template --name $(OPEN_MATCH_CHART_NAME) --namespace $(OPEN_MATCH_KUBERNETES_NAMESPACE) \
--set redis.fullnameOverride='$(REDIS_NAME)' \
--set openmatch.config.install=false \
--set openmatch.backendapi.install=false \
--set openmatch.frontendapi.install=false \
--set openmatch.mmlogicapi.install=false \
--set prometheus.enabled=false \
--set grafana.enabled=false \
install/helm/open-match > install/yaml/01-redis-chart.yaml
install/yaml/02-open-match.yaml: build/toolchain/bin/helm$(EXE_EXTENSION)
mkdir -p install/yaml/
$(HELM) template --name $(OPEN_MATCH_CHART_NAME) --namespace $(OPEN_MATCH_KUBERNETES_NAMESPACE) \
--set redis.fullnameOverride='$(REDIS_NAME)' \
--set redis.enabled=false \
--set prometheus.enabled=false \
--set grafana.enabled=false \
--set openmatch.image.registry=$(REGISTRY) \
--set openmatch.image.tag=$(TAG) \
--set openmatch.noChartMeta=true \
install/helm/open-match > install/yaml/02-open-match.yaml
install/yaml/03-prometheus-chart.yaml: build/toolchain/bin/helm$(EXE_EXTENSION)
mkdir -p install/yaml/
$(HELM) template --name $(OPEN_MATCH_CHART_NAME) --namespace $(OPEN_MATCH_KUBERNETES_NAMESPACE) \
--set redis.enabled=false \
--set openmatch.config.install=false \
--set openmatch.backendapi.install=false \
--set openmatch.frontendapi.install=false \
--set openmatch.mmlogicapi.install=false \
--set grafana.enabled=false \
install/helm/open-match > install/yaml/03-prometheus-chart.yaml
install/yaml/04-grafana-chart.yaml: build/toolchain/bin/helm$(EXE_EXTENSION)
mkdir -p install/yaml/
$(HELM) template --name $(OPEN_MATCH_CHART_NAME) --namespace $(OPEN_MATCH_KUBERNETES_NAMESPACE) \
--set redis.enabled=false \
--set openmatch.config.install=false \
--set openmatch.backendapi.install=false \
--set openmatch.frontendapi.install=false \
--set openmatch.mmlogicapi.install=false \
--set prometheus.enabled=false \
--set grafana.enabled=true \
install/helm/open-match > install/yaml/04-grafana-chart.yaml
install/yaml/install.yaml: build/toolchain/bin/helm$(EXE_EXTENSION)
mkdir -p install/yaml/
$(HELM) template --name $(OPEN_MATCH_CHART_NAME) --namespace $(OPEN_MATCH_KUBERNETES_NAMESPACE) \
--set openmatch.image.registry=$(REGISTRY) \
--set openmatch.image.tag=$(TAG) \
--set redis.enabled=true \
--set prometheus.enabled=true \
--set grafana.enabled=true \
install/helm/open-match > install/yaml/install.yaml
install/yaml/install-example.yaml: build/toolchain/bin/helm$(EXE_EXTENSION)
mkdir -p install/yaml/
$(HELM) template --name $(OPEN_MATCH_EXAMPLE_CHART_NAME) --namespace $(OPEN_MATCH_EXAMPLE_KUBERNETES_NAMESPACE) \
--set openmatch.image.registry=$(REGISTRY) \
--set openmatch.image.tag=$(TAG) \
install/helm/open-match-example > install/yaml/install-example.yaml
set-redis-password:
@stty -echo; \
printf "Redis password: "; \
read REDIS_PASSWORD; \
stty echo; \
printf "\n"; \
REDIS_PASSWORD=$$(printf "$$REDIS_PASSWORD" | base64); \
printf "apiVersion: v1\nkind: Secret\nmetadata:\n name: $(REDIS_NAME)\n namespace: $(OPEN_MATCH_KUBERNETES_NAMESPACE)\ndata:\n redis-password: $$REDIS_PASSWORD\n" | \
$(KUBECTL) replace -f - --force
install-toolchain: build/toolchain/bin/protoc$(EXE_EXTENSION) build/toolchain/bin/protoc-gen-go$(EXE_EXTENSION) build/toolchain/bin/kubectl$(EXE_EXTENSION) build/toolchain/bin/helm$(EXE_EXTENSION) build/toolchain/bin/minikube$(EXE_EXTENSION) build/toolchain/bin/skaffold$(EXE_EXTENSION) build/toolchain/bin/hugo$(EXE_EXTENSION) build/toolchain/bin/protoc-gen-grpc-gateway$(EXE_EXTENSION) build/toolchain/bin/htmltest$(EXE_EXTENSION)
build/toolchain/bin/helm$(EXE_EXTENSION):
mkdir -p $(TOOLCHAIN_BIN)
mkdir -p $(TOOLCHAIN_DIR)/temp-helm
cd $(TOOLCHAIN_DIR)/temp-helm && curl -Lo helm.tar.gz $(HELM_PACKAGE) && tar xzf helm.tar.gz --strip-components 1
mv $(TOOLCHAIN_DIR)/temp-helm/helm$(EXE_EXTENSION) $(TOOLCHAIN_BIN)/helm$(EXE_EXTENSION)
mv $(TOOLCHAIN_DIR)/temp-helm/tiller$(EXE_EXTENSION) $(TOOLCHAIN_BIN)/tiller$(EXE_EXTENSION)
rm -rf $(TOOLCHAIN_DIR)/temp-helm/
build/toolchain/bin/hugo$(EXE_EXTENSION):
mkdir -p $(TOOLCHAIN_BIN)
mkdir -p $(TOOLCHAIN_DIR)/temp-hugo
cd $(TOOLCHAIN_DIR)/temp-hugo && curl -Lo hugo.tar.gz $(HUGO_PACKAGE) && tar xzf hugo.tar.gz
mv $(TOOLCHAIN_DIR)/temp-hugo/hugo$(EXE_EXTENSION) $(TOOLCHAIN_BIN)/hugo$(EXE_EXTENSION)
rm -rf $(TOOLCHAIN_DIR)/temp-hugo/
build/toolchain/bin/minikube$(EXE_EXTENSION):
mkdir -p $(TOOLCHAIN_BIN)
curl -Lo minikube$(EXE_EXTENSION) $(MINIKUBE_PACKAGE)
chmod +x minikube$(EXE_EXTENSION)
mv minikube$(EXE_EXTENSION) $(TOOLCHAIN_BIN)/minikube$(EXE_EXTENSION)
build/toolchain/bin/kubectl$(EXE_EXTENSION):
mkdir -p $(TOOLCHAIN_BIN)
curl -Lo kubectl$(EXE_EXTENSION) $(KUBECTL_PACKAGE)
chmod +x kubectl$(EXE_EXTENSION)
mv kubectl$(EXE_EXTENSION) $(TOOLCHAIN_BIN)/kubectl$(EXE_EXTENSION)
build/toolchain/bin/skaffold$(EXE_EXTENSION):
mkdir -p $(TOOLCHAIN_BIN)
curl -Lo skaffold$(EXE_EXTENSION) $(SKAFFOLD_PACKAGE)
chmod +x skaffold$(EXE_EXTENSION)
mv skaffold$(EXE_EXTENSION) $(TOOLCHAIN_BIN)/skaffold$(EXE_EXTENSION)
build/toolchain/bin/htmltest$(EXE_EXTENSION):
mkdir -p $(TOOLCHAIN_BIN)
mkdir -p $(TOOLCHAIN_DIR)/temp-htmltest
cd $(TOOLCHAIN_DIR)/temp-htmltest && curl -Lo htmltest.tar.gz $(HTMLTEST_PACKAGE) && tar xzf htmltest.tar.gz
mv $(TOOLCHAIN_DIR)/temp-htmltest/htmltest$(EXE_EXTENSION) $(TOOLCHAIN_BIN)/htmltest$(EXE_EXTENSION)
rm -rf $(TOOLCHAIN_DIR)/temp-htmltest/
build/toolchain/bin/golangci-lint$(EXE_EXTENSION):
mkdir -p $(TOOLCHAIN_BIN)
mkdir -p $(TOOLCHAIN_DIR)/temp-golangci
cd $(TOOLCHAIN_DIR)/temp-golangci && curl -Lo golangci.tar.gz $(GOLANGCI_PACKAGE) && tar xvzf golangci.tar.gz --strip-components 1
mv $(TOOLCHAIN_DIR)/temp-golangci/golangci-lint$(EXE_EXTENSION) $(TOOLCHAIN_BIN)/golangci-lint$(EXE_EXTENSION)
rm -rf $(TOOLCHAIN_DIR)/temp-golangci/
build/toolchain/bin/protoc$(EXE_EXTENSION):
mkdir -p $(TOOLCHAIN_BIN)
curl -o $(TOOLCHAIN_DIR)/protoc-temp.zip -L $(PROTOC_PACKAGE)
(cd $(TOOLCHAIN_DIR); unzip -q -o protoc-temp.zip)
rm $(TOOLCHAIN_DIR)/protoc-temp.zip $(TOOLCHAIN_DIR)/readme.txt
build/toolchain/bin/protoc-gen-go$(EXE_EXTENSION):
mkdir -p $(TOOLCHAIN_BIN)
cd $(TOOLCHAIN_BIN) && $(GO) build -pkgdir . github.com/golang/protobuf/protoc-gen-go
build/toolchain/bin/protoc-gen-grpc-gateway$(EXE_EXTENSION):
mkdir -p $(TOOLCHAIN_DIR)/googleapis-temp/
mkdir -p $(TOOLCHAIN_BIN)
curl -o $(TOOLCHAIN_DIR)/googleapis-temp/googleapis.zip -L \
https://github.com/googleapis/googleapis/archive/master.zip
(cd $(TOOLCHAIN_DIR)/googleapis-temp/; unzip -q -o googleapis.zip)
cp -rf $(TOOLCHAIN_DIR)/googleapis-temp/googleapis-master/google/api/ \
$(PROTOC_INCLUDES)/google/api
rm -rf $(TOOLCHAIN_DIR)/googleapis-temp
cd $(TOOLCHAIN_BIN) && $(GO) build -pkgdir . github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
cd $(TOOLCHAIN_BIN) && $(GO) build -pkgdir . github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
build/archives/$(NODEJS_PACKAGE_NAME):
mkdir -p build/archives/
cd build/archives/ && curl -L -o $(NODEJS_PACKAGE_NAME) $(NODEJS_PACKAGE)
build/toolchain/nodejs/: build/archives/$(NODEJS_PACKAGE_NAME)
mkdir -p build/toolchain/nodejs/
cd build/toolchain/nodejs/ && tar xzf ../../archives/$(NODEJS_PACKAGE_NAME) --strip-components 1
push-helm: build/toolchain/bin/helm$(EXE_EXTENSION) build/toolchain/bin/kubectl$(EXE_EXTENSION)
$(KUBECTL) create serviceaccount --namespace kube-system tiller
$(HELM) init --service-account tiller --force-upgrade
$(KUBECTL) create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
ifneq ($(strip $($(KUBECTL) get clusterroles | grep -i rbac)),)
$(KUBECTL) patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'
endif
echo "Waiting for Tiller to become ready..."
$(KUBECTL) wait deployment --timeout=60s --for condition=available -l app=helm,name=tiller --namespace kube-system
delete-helm: build/toolchain/bin/helm$(EXE_EXTENSION) build/toolchain/bin/kubectl$(EXE_EXTENSION)
-$(HELM) reset
-$(KUBECTL) delete serviceaccount --namespace kube-system tiller
-$(KUBECTL) delete clusterrolebinding tiller-cluster-rule
ifneq ($(strip $($(KUBECTL) get clusterroles | grep -i rbac)),)
-$(KUBECTL) delete deployment --namespace kube-system tiller-deploy
endif
echo "Waiting for Tiller to go away..."
-$(KUBECTL) wait deployment --timeout=60s --for delete -l app=helm,name=tiller --namespace kube-system
# Fake target for docker
docker: no-sudo
# Fake target for gcloud
gcloud: no-sudo
auth-docker: gcloud docker
gcloud $(GCP_PROJECT_FLAG) auth configure-docker
auth-gke-cluster: gcloud
gcloud $(GCP_PROJECT_FLAG) container clusters get-credentials $(GKE_CLUSTER_NAME) $(GCP_LOCATION_FLAG)
create-gke-cluster: build/toolchain/bin/kubectl$(EXE_EXTENSION) gcloud
gcloud $(GCP_PROJECT_FLAG) container clusters create $(GKE_CLUSTER_NAME) $(GCP_LOCATION_FLAG) --machine-type n1-standard-4 --tags open-match $(KUBERNETES_COMPAT)
$(KUBECTL) create clusterrolebinding myname-cluster-admin-binding --clusterrole=cluster-admin --user=$(GCLOUD_ACCOUNT_EMAIL)
delete-gke-cluster: gcloud
gcloud $(GCP_PROJECT_FLAG) container clusters delete $(GKE_CLUSTER_NAME) $(GCP_LOCATION_FLAG) --quiet
create-mini-cluster: build/toolchain/bin/minikube$(EXE_EXTENSION)
$(MINIKUBE) start --memory 6144 --cpus 4 --disk-size 50g
delete-mini-cluster: build/toolchain/bin/minikube$(EXE_EXTENSION)
$(MINIKUBE) delete
all-protos: golang-protos reverse-golang-protos swagger-def-protos
golang-protos: internal/pb/backend.pb.go internal/pb/frontend.pb.go internal/pb/matchfunction.pb.go internal/pb/messages.pb.go internal/pb/mmlogic.pb.go
reverse-golang-protos: internal/pb/backend.pb.gw.go internal/pb/frontend.pb.gw.go internal/pb/matchfunction.pb.gw.go internal/pb/messages.pb.gw.go internal/pb/mmlogic.pb.gw.go
swagger-def-protos: internal/swagger/frontend.proto internal/swagger/backend.proto internal/swagger/mmlogic.proto internal/swagger/matchfunction.proto
internal/pb/%.pb.go: api/protobuf-spec/%.proto build/toolchain/bin/protoc$(EXE_EXTENSION) build/toolchain/bin/protoc-gen-go$(EXE_EXTENSION) build/toolchain/bin/protoc-gen-grpc-gateway$(EXE_EXTENSION)
$(PROTOC) $< \
-I $(REPOSITORY_ROOT) -I $(PROTOC_INCLUDES) \
--go_out=plugins=grpc:$(REPOSITORY_ROOT)
internal/pb/%.pb.gw.go: api/protobuf-spec/%.proto build/toolchain/bin/protoc$(EXE_EXTENSION) build/toolchain/bin/protoc-gen-go$(EXE_EXTENSION) build/toolchain/bin/protoc-gen-grpc-gateway$(EXE_EXTENSION)
$(PROTOC) $< \
-I $(REPOSITORY_ROOT) -I $(PROTOC_INCLUDES) \
--grpc-gateway_out=logtostderr=true,allow_delete_body=true:$(REPOSITORY_ROOT)\
internal/swagger/%.proto: api/protobuf-spec/%.proto build/toolchain/bin/protoc$(EXE_EXTENSION) build/toolchain/bin/protoc-gen-grpc-gateway$(EXE_EXTENSION)
$(PROTOC) $< \
-I $(REPOSITORY_ROOT) -I $(PROTOC_INCLUDES) \
--swagger_out=logtostderr=true,allow_delete_body=true:.
# Include structure of the protos needs to be called out do the dependency chain is run through properly.
internal/pb/backend.pb.go: internal/pb/messages.pb.go
internal/pb/frontend.pb.go: internal/pb/messages.pb.go
internal/pb/mmlogic.pb.go: internal/pb/messages.pb.go
internal/pb/matchfunction.pb.go: internal/pb/messages.pb.go
build:
$(GO) build ./...
test:
$(GO) test ./... -race
test-in-ci:
$(GO) test ./... -race -test.count 25 -cover
fmt:
$(GO) fmt ./...
vet:
$(GO) vet ./...
# Blocked on https://github.com/golangci/golangci-lint/issues/500
golangci: build/toolchain/bin/golangci-lint$(EXE_EXTENSION)
build/toolchain/bin/golangci-lint$(EXE_EXTENSION) run -v --config=.golangci.yaml
lint: fmt vet lint-chart
cmd/minimatch/minimatch: internal/pb/backend.pb.go internal/pb/frontend.pb.go internal/pb/mmlogic.pb.go internal/pb/matchfunction.pb.go internal/pb/messages.pb.go
cd cmd/minimatch; $(GO_BUILD_COMMAND)
cmd/backendapi/backendapi: internal/pb/backend.pb.go
cd cmd/backendapi; $(GO_BUILD_COMMAND)
cmd/frontendapi/frontendapi: internal/pb/frontend.pb.go
cd cmd/frontendapi; $(GO_BUILD_COMMAND)
cmd/mmlogicapi/mmlogicapi: internal/pb/mmlogic.pb.go
cd cmd/mmlogicapi; $(GO_BUILD_COMMAND)
examples/backendclient/backendclient: internal/pb/backend.pb.go
cd examples/backendclient; $(GO_BUILD_COMMAND)
examples/evaluators/golang/serving/serving: internal/pb/messages.pb.go
cd examples/evaluators/golang/serving; $(GO_BUILD_COMMAND)
examples/functions/golang/grpc-serving/grpc-serving: internal/pb/messages.pb.go
cd examples/functions/golang/grpc-serving; $(GO_BUILD_COMMAND)
test/cmd/clientloadgen/clientloadgen:
cd test/cmd/clientloadgen; $(GO_BUILD_COMMAND)
test/cmd/frontendclient/frontendclient: internal/pb/frontend.pb.go internal/pb/messages.pb.go
cd test/cmd/frontendclient; $(GO_BUILD_COMMAND)
node_modules/: build/toolchain/nodejs/
-rm -r package.json package-lock.json
-rm -rf node_modules/
echo "{}" > package.json
-rm -f package-lock.json
$(TOOLCHAIN_DIR)/nodejs/bin/npm install postcss-cli autoprefixer
build/site/: build/toolchain/bin/hugo$(EXE_EXTENSION) node_modules/
rm -rf build/site/
mkdir -p build/site/
cd site/ && ../build/toolchain/bin/hugo$(EXE_EXTENSION) --config=config.toml --source . --destination $(BUILD_DIR)/site/public/
# Only copy the root directory since that has the AppEngine serving code.
-cp -f site/* $(BUILD_DIR)/site
-cp -f site/.gcloudignore $(BUILD_DIR)/site/.gcloudignore
#cd $(BUILD_DIR)/site && "SERVICE=$(SERVICE) envsubst < app.yaml > .app.yaml"
cp $(BUILD_DIR)/site/app.yaml $(BUILD_DIR)/site/.app.yaml
site-test: TEMP_SITE_DIR := /tmp/open-match-site
site-test: build/site/ build/toolchain/bin/htmltest$(EXE_EXTENSION)
rm -rf $(TEMP_SITE_DIR)
mkdir -p $(TEMP_SITE_DIR)/site/
cp -rf $(REPOSITORY_ROOT)/build/site/public/* $(TEMP_SITE_DIR)/site/
$(HTMLTEST) --conf $(REPOSITORY_ROOT)/site/htmltest.yaml $(TEMP_SITE_DIR)
browse-site: build/site/
cd $(BUILD_DIR)/site && dev_appserver.py .app.yaml
deploy-dev-site: build/site/ gcloud
cd $(BUILD_DIR)/site && gcloud $(OM_SITE_GCP_PROJECT_FLAG) app deploy .app.yaml --promote --version=$(VERSION_SUFFIX) --quiet
ci-deploy-dev-site: build/site/ gcloud
ifeq ($(_GCB_POST_SUBMIT),1)
echo "Deploying website to development.open-match.dev..."
# TODO: Install GAE SDK and use the Service Account to deploy to GAE.
#cd $(BUILD_DIR)/site && gcloud $(OM_SITE_GCP_PROJECT_FLAG) app deploy .app.yaml --promote --version=$(VERSION_SUFFIX) --quiet
else
echo "Not deploying development.open-match.dev because this is not a post commit change."
endif
deploy-redirect-site: gcloud
cd $(REPOSITORY_ROOT)/site/redirect/ && gcloud $(OM_SITE_GCP_PROJECT_FLAG) app deploy app.yaml --promote --quiet
run-site: build/toolchain/bin/hugo$(EXE_EXTENSION)
cd site/ && ../build/toolchain/bin/hugo$(EXE_EXTENSION) server --debug --watch --enableGitInfo . --baseURL=http://localhost:$(SITE_PORT)/ --bind 0.0.0.0 --port $(SITE_PORT) --disableFastRender
ci-deploy-artifacts: install/yaml/ gcloud
ifeq ($(_GCB_POST_SUBMIT),1)
#gsutil cp -a public-read $(REPOSITORY_ROOT)/install/yaml/* gs://open-match-chart/install/$(VERSION_SUFFIX)/
gsutil cp -a public-read $(REPOSITORY_ROOT)/install/yaml/* gs://open-match-chart/install/yaml/$(BRANCH_NAME)-latest/
else
echo "Not deploying development.open-match.dev because this is not a post commit change."
endif
all: service-binaries client-binaries example-binaries
service-binaries: cmd/minimatch/minimatch cmd/backendapi/backendapi cmd/frontendapi/frontendapi cmd/mmlogicapi/mmlogicapi
client-binaries: examples/backendclient/backendclient test/cmd/clientloadgen/clientloadgen test/cmd/frontendclient/frontendclient
example-binaries: example-mmf-binaries example-evaluator-binaries
example-mmf-binaries: examples/functions/golang/grpc-serving/grpc-serving
example-evaluator-binaries: examples/evaluators/golang/serving/serving
# For presubmit we want to update the protobuf generated files and verify that tests are good.
presubmit: sync-deps clean-protos all-protos fmt vet build test
build/release/: presubmit clean-install-yaml install/yaml/
mkdir -p $(BUILD_DIR)/release/
cp install/yaml/* $(BUILD_DIR)/release/
release: REGISTRY = gcr.io/$(OPEN_MATCH_PUBLIC_IMAGES_PROJECT_ID)
release: TAG = $(BASE_VERSION)
release: build/release/
clean-release:
rm -rf build/release/
clean-site:
rm -rf build/site/
clean-protos:
rm -rf internal/pb/
rm -rf api/protobuf_spec/
clean-binaries:
rm -rf cmd/minimatch/minimatch
rm -rf cmd/backendapi/backendapi
rm -rf cmd/frontendapi/frontendapi
rm -rf cmd/mmlogicapi/mmlogicapi
rm -rf examples/backendclient/backendclient
rm -rf examples/evaluators/golang/serving/serving
rm -rf examples/functions/golang/grpc-serving/grpc-serving
rm -rf test/cmd/clientloadgen/clientloadgen
rm -rf test/cmd/frontendclient/frontendclient
clean-toolchain:
rm -rf build/toolchain/
clean-nodejs:
rm -rf build/toolchain/nodejs/
rm -rf node_modules/
rm -rf package.json
rm -rf package-lock.json
clean-install-yaml:
rm -f install/yaml/install.yaml
rm -f install/yaml/install-example.yaml
rm -f install/yaml/01-redis-chart.yaml
rm -f install/yaml/02-open-match.yaml
rm -f install/yaml/03-prometheus-chart.yaml
rm -f install/yaml/04-grafana-chart.yaml
clean: clean-images clean-binaries clean-site clean-release clean-toolchain clean-protos clean-nodejs clean-install-yaml
run-backendclient: build/toolchain/bin/kubectl$(EXE_EXTENSION)
$(KUBECTL) run om-backendclient --rm --restart=Never --image-pull-policy=Always -i --tty --image=$(REGISTRY)/openmatch-backendclient:$(TAG) --namespace=$(OPEN_MATCH_KUBERNETES_NAMESPACE) $(KUBECTL_RUN_ENV)
run-frontendclient: build/toolchain/bin/kubectl$(EXE_EXTENSION)
$(KUBECTL) run om-frontendclient --rm --restart=Never --image-pull-policy=Always -i --tty --image=$(REGISTRY)/openmatch-frontendclient:$(TAG) --namespace=$(OPEN_MATCH_KUBERNETES_NAMESPACE) $(KUBECTL_RUN_ENV)
run-clientloadgen: build/toolchain/bin/kubectl$(EXE_EXTENSION)
$(KUBECTL) run om-clientloadgen --rm --restart=Never --image-pull-policy=Always -i --tty --image=$(REGISTRY)/openmatch-clientloadgen:$(TAG) --namespace=$(OPEN_MATCH_KUBERNETES_NAMESPACE) $(KUBECTL_RUN_ENV)
proxy-grafana: build/toolchain/bin/kubectl$(EXE_EXTENSION)
echo "User: admin"
echo "Password: openmatch"
$(KUBECTL) port-forward --namespace $(OPEN_MATCH_KUBERNETES_NAMESPACE) $(shell $(KUBECTL) get pod --namespace $(OPEN_MATCH_KUBERNETES_NAMESPACE) --selector="app=grafana,release=$(OPEN_MATCH_CHART_NAME)" --output jsonpath='{.items[0].metadata.name}') $(GRAFANA_PORT):3000 $(PORT_FORWARD_ADDRESS_FLAG)
proxy-prometheus: build/toolchain/bin/kubectl$(EXE_EXTENSION)
$(KUBECTL) port-forward --namespace $(OPEN_MATCH_KUBERNETES_NAMESPACE) $(shell $(KUBECTL) get pod --namespace $(OPEN_MATCH_KUBERNETES_NAMESPACE) --selector="app=prometheus,component=server,release=$(OPEN_MATCH_CHART_NAME)" --output jsonpath='{.items[0].metadata.name}') $(PROMETHEUS_PORT):9090 $(PORT_FORWARD_ADDRESS_FLAG)
proxy-dashboard: build/toolchain/bin/kubectl$(EXE_EXTENSION)
$(KUBECTL) port-forward --namespace kube-system $(shell $(KUBECTL) get pod --namespace kube-system --selector="app=kubernetes-dashboard" --output jsonpath='{.items[0].metadata.name}') $(DASHBOARD_PORT):9090 $(PORT_FORWARD_ADDRESS_FLAG)
sync-deps:
$(GO) mod download
sleep-10:
sleep 10
# Prevents users from running with sudo.
# There's an exception for Google Cloud Build because it runs as root.
no-sudo:
ifndef ALLOW_BUILD_WITH_SUDO
ifeq ($(shell whoami),root)
echo "ERROR: Running Makefile as root (or sudo)"
echo "Please follow the instructions at https://docs.docker.com/install/linux/linux-postinstall/ if you are trying to sudo run the Makefile because of the 'Cannot connect to the Docker daemon' error."
echo "NOTE: sudo/root do not have the authentication token to talk to any GCP service via gcloud."
exit 1
endif
endif
.PHONY: docker gcloud deploy-redirect-site sync-deps sleep-10 proxy-dashboard proxy-prometheus proxy-grafana clean clean-toolchain clean-binaries clean-protos presubmit test test-in-ci vet

300
README.md

@ -1,246 +1,162 @@
# Open Match
![Open Match](site/static/images/logo-with-name.png)
Open Match is an open source game matchmaker designed to allow game creators to re-use a common matchmaker framework. Its designed to be flexible (run it anywhere Kubernetes runs), extensible (match logic can be customized to work for any game), and scalable.
[![GoDoc](https://godoc.org/github.com/GoogleCloudPlatform/open-match?status.svg)](https://godoc.org/github.com/GoogleCloudPlatform/open-match)
[![Go Report Card](https://goreportcard.com/badge/github.com/GoogleCloudPlatform/open-match)](https://goreportcard.com/report/github.com/GoogleCloudPlatform/open-match)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/GoogleCloudPlatform/open-match/blob/master/LICENSE)
Matchmaking is a complicated process, and when large player populations are involved, many popular matchmaking approaches touch on significant areas of computer science including graph theory and massively concurrent processing. Open Match is an effort to provide a foundation upon which these difficult problems can be addressed by the wider game development community. As Josh Menke &mdash; famous for working on matchmaking for many popular triple-A franchises &mdash; put it:
Open Match is an open source game matchmaking framework designed to allow game creators to build matchmakers of any size easily and with as much possibility for sharing and code re-use as possible. Its designed to be flexible, extensible, and scalable.
Matchmaking begins when a player tells the game that they want to play. Every player has a set of attributes like skill, location, playtime, win-lose ratio, etc which may factor in how they are paired with other players. Typically, there's a trade off between the quality of the match vs the time to wait. Since Open Match is designed to scale with the player population, it should be possible to still have high quality matches while having high player count.
Under the covers matchmaking approaches touch on significant areas of computer science including graph theory and massively concurrent processing. Open Match is an effort to provide a foundation upon which these difficult problems can be addressed by the wider game development community. As Josh Menke &mdash; famous for working on matchmaking for many popular triple-A franchises &mdash; put it:
["Matchmaking, a lot of it actually really is just really good engineering. There's a lot of really hard networking and plumbing problems that need to be solved, depending on the size of your audience."](https://youtu.be/-pglxege-gU?t=830)
This project attempts to solve the networking and plumbing problems, so game developers can focus on the logic to match players into great games.
## Disclaimer
This software is currently alpha, and subject to change. **It is not yet ready to be used in production.**
## Running Open Match
Open Match framework is a collection of servers that run within Kubernetes (the [puppet master](https://en.wikipedia.org/wiki/Puppet_Master_(gaming)) for your server cluster.)
## Version
The current stable version in master is 0.1.0.
The 0.2.0 RC1 is now available.
# Core Concepts
## Deploy to Kubernetes
[Watch the introduction of Open Match at Unite Berlin 2018 on YouTube](https://youtu.be/qasAmy_ko2o)
If you have an [existing Kubernetes cluster](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-cluster) you can run these commands to install Open Match.
```bash
# Grant yourself cluster-admin permissions so that you can deploy service accounts.
kubectl create clusterrolebinding myname-cluster-admin-binding --clusterrole=cluster-admin --user=$(YOUR_KUBERNETES_USER_NAME)
# Place all Open Match components in their own namespace.
kubectl create namespace open-match
# Install Open Match and monitoring services.
kubectl apply -f https://storage.googleapis.com/open-match-chart/install/yaml/master-latest/install.yaml --namespace open-match
# Install the example MMF and Evaluator.
kubectl apply -f https://storage.googleapis.com/open-match-chart/install/yaml/master-latest/install-example.yaml --namespace open-match
```
Open Match is designed to support massively concurrent matchmaking, and to be scalable to player populations of hundreds of millions or more. It attempts to apply stateless web tech microservices patterns to game matchmaking. If you're not sure what that means, that's okay &mdash; it is fully open source and designed to be customizable to fit into your online game architecture &mdash; so have a look a the code and modify it as you see fit.
To delete Open Match
## Glossary
```bash
# Delete the open-match namespace that holds all the Open Match configuration.
kubectl delete namespace open-match
```
* **MMF** &mdash; Matchmaking function. This is the customizable matchmaking logic.
* **Component** &mdash; One of the discrete processes in an Open Match deployment. Open Match is composed of multiple scalable microservices called 'components'.
* **Roster** &mdash; A list of all the players in a match.
* **Profile** &mdash; The json blob containing all the parameters used to select which players go into a roster.
* **Match Object** &mdash; A protobuffer message format that contains the Profile and the results of the matchmaking function. Sent to the backend API from your game backend with an empty roster and then returned from your MMF with the matchmaking results filled in.
* **MMFOrc** &mdash; Matchmaker function orchestrator. This Open Match core component is in charge of kicking off custom matchmaking functions (MMFs) and evaluator processes.
* **State Storage** &mdash; The storage software used by Open Match to hold all the matchmaking state. Open Match ships with [Redis](https://redis.io/) as the default state storage.
* **Assignment** &mdash; Refers to assigning a player or group of players to a dedicated game server instance. Open Match offers a path to send dedicated game server connection details from your backend to your game clients after a match has been made.
## Development
Open Match can be deployed locally or in the cloud for development. Below are the steps to build, push, and deploy the binaries to Kubernetes.
## Requirements
* [Kubernetes](https://kubernetes.io/) cluster &mdash; tested with version 1.9.
* [Redis 4+](https://redis.io/) &mdash; tested with 4.0.11.
* Open Match is compiled against the latest release of [Golang](https://golang.org/) &mdash; tested with 1.10.3.
### Deploy to Minikube (Locally)
[Minikube](https://kubernetes.io/docs/setup/minikube/) is Kubernetes in a VM. It's mainly used for development.
## Components
```bash
# Create a Minikube Cluster and install Helm
make create-mini-cluster push-helm
# Deploy Open Match with example functions
make REGISTRY=gcr.io/open-match-public-images TAG=latest install-chart install-example-chart
```
Open Match is a set of processes designed to run on Kubernetes. It contains these **core** components:
### Deploy to Google Cloud Platform (Cloud)
1. Frontend API
1. Backend API
1. Matchmaker Function Orchestrator (MMFOrc)
1. Matchmaking Logic (MMLogic) API
Create a GCP project via [Google Cloud Console](https://console.cloud.google.com/). Billing must be enabled but if you're a new customer you can get some [free credits](https://cloud.google.com/free/). When you create a project you'll need to set a Project ID, if you forget it you can see it here, https://console.cloud.google.com/iam-admin/settings/project.
It also explicitly depends on these two **customizable** components.
Now install [Google Cloud SDK](https://cloud.google.com/sdk/) which is the command line tool to work against your project. The following commands log you into your GCP Project.
1. Matchmaking "Function" (MMF)
1. Evaluator (may be deprecated in future versions)
```bash
# Login to your Google Account for GCP.
gcloud auth login
gcloud config set project $YOUR_GCP_PROJECT_ID
# Enable GCP services
gcloud services enable containerregistry.googleapis.com
gcloud services enable container.googleapis.com
# Test that everything is good, this command should work.
gcloud compute zones list
```
While **core** components are fully open source and *can* be modified, they are designed to support the majority of matchmaking scenarios *without need to change the source code*. The Open Match repository ships with simple **customizable** example MMF and Evaluator processes, but it is expected that most users will want full control over the logic in these, so they have been designed to be as easy to modify or replace as possible.
Please follow the instructions to [Setup Local Open Match Repository](#local-repository-setup). Once everything is setup you can deploy Open Match by creating a cluster in Google Kubernetes Engine (GKE).
### Frontend API
```bash
# Create a GKE Cluster and install Helm
make create-gke-cluster push-helm
# Deploy Open Match with example functions
make REGISTRY=gcr.io/open-match-build TAG=0.4.0-e98e1b6 install-chart install-example-chart
```
The Frontend API accepts the player data and puts it in state storage so your Matchmaking Function (MMF) can access it.
To generate matches using a test client, run the following command:
The Frontend API is a server application that implements the [gRPC](https://grpc.io/) service defined in `api/protobuf-spec/frontend.proto`. At the most basic level, it expects clients to connect and send:
* A **unique ID** for the group of players (the group can contain any number of players, including only one).
* A **json blob** containing all player-related data you want to use in your matchmaking function.
```bash
make REGISTRY=gcr.io/open-match-build TAG=0.4.0-e98e1b6 run-backendclient
```
The client is expected to maintain a connection, waiting for an update from the API that contains the details required to connect to a dedicated game server instance (an 'assignment'). There are also basic functions for removing an ID from the matchmaking pool or an existing match.
Once deployed you can view the jobs in [Cloud Console](https://console.cloud.google.com/kubernetes/workload).
### Backend API
### Local Repository Setup
The Backend API puts match profiles in state storage which the Matchmaking Function (MMF) can access and use to decide which players should be put into a match together, then return those matches to dedicated game server instances.
Here are the instructions to set up a local repository for Open Match.
The Backend API is a server application that implements the [gRPC](https://grpc.io/) service defined in `api/protobuf-spec/backend.proto`. At the most basic level, it expects to be connected to your online infrastructure (probably to your server scaling manager or scheduler, or even directly to a dedicated game server), and to receive:
* A **unique ID** for a matchmaking profile.
* A **json blob** containing all the match-related data you want to use in your matchmaking function, in an 'empty' match object.
```bash
# Install Open Match Toolchain Dependencies (for Debian, other OSes including Mac OS X have similar dependencies)
sudo apt-get update; sudo apt-get install -y -q python3 python3-virtualenv virtualenv make google-cloud-sdk git unzip tar
# Setup your repository like Go workspace, https://golang.org/doc/code.html#Workspaces
# This requirement will go away soon.
mkdir -p $HOME/workspace/src/github.com/GoogleCloudPlatform/
cd $HOME/workspace/src/github.com/GoogleCloudPlatform/
export GOPATH=$HOME/workspace
export GO111MODULE=on
git clone https://github.com/GoogleCloudPlatform/open-match.git
cd open-match
```
Your game backend is expected to maintain a connection, waiting for 'filled' match objects containing a roster of players. The Backend API also provides a return path for your game backend to return dedicated game server connection details (an 'assignment') to the game client, and to delete these 'assignments'.
### Compiling From Source
The easiest way to build Open Match is to use the [Makefile](Makefile). Please follow the instructions to [Setup Local Open Match Repository](#local-repository-setup).
### Matchmaking Function Orchestrator (MMFOrc)
[Docker](https://docs.docker.com/install/) and [Go 1.12+](https://golang.org/dl/) is also required.
The MMFOrc kicks off your custom matchmaking function (MMF) for every profile submitted to the Backend API. It also runs the Evaluator to resolve conflicts in case more than one of your profiles matched the same players.
To build all the artifacts of Open Match you can simply run the following commands.
The MMFOrc exists to orchestrate/schedule your **custom components**, running them as often as required to meet the demands of your game. MMFOrc runs in an endless loop, submitting MMFs and Evaluator jobs to Kubernetes.
```bash
# Downloads all the tools needed to build Open Match
make install-toolchain
# Generates protocol buffer code files
make all-protos
# Builds all the binaries
make all
# Builds all the images.
make build-images
```
### Matchmaking Logic (MMLogic) API
Once build you can use a command like `docker images` to see all the images that were build.
The MMLogic API provides a series of gRPC functions that act as a Matchmaking Function SDK. Much of the basic, boilerplate code for an MMF is the same regardless of what players you want to match together. The MMLogic API offers a gRPC interface for many common MMF tasks, such as:
Before creating a pull request you can run `make local-cloud-build` to simulate a Cloud Build run to check for regressions.
1. Reading a profile from state storage.
1. Running filters on players in state strorage.
1. Removing chosen players from consideration by other MMFs (by adding them to an ignore list).
1. Writing the matchmaking results to state storage.
1. (Optional, NYI) Exporting MMF stats for metrics collection.
The directory structure is a typical Go structure so if you do the following you should be able to work on this project within your IDE.
More details about the available gRPC calls can be found in the [API Specification](api/protobuf-spec/messages.proto).
Lastly, this project uses go modules so you'll want to set `export GO111MODULE=on` in your `~/.bashrc`.
**Note**: using the MMLogic API is **optional**. It tries to simplify the development of MMFs, but if you want to take care of these tasks on your own, you can make few or no calls to the MMLogic API as long as your MMF still completes all the required tasks. Read the [Matchmaking Functions section](#matchmaking-functions-mmfs) for more details of what work an MMF must do.
The [Build Queue](https://console.cloud.google.com/cloud-build/builds?project=open-match-build) runs against all PRs, requires membership to [open-match-discuss@googlegroups.com](https://groups.google.com/forum/#!forum/open-match-discuss).
### Evaluator
## Support
The Evaluator resolves conflicts when multiple matches want to include the same player(s).
The Evaluator is a component run by the Matchmaker Function Orchestrator (MMFOrc) after the matchmaker functions have been run, and some proposed results are available. The Evaluator looks at all the proposed matches, and if multiple proposals contain the same player(s), it breaks the tie. In many simple matchmaking setups with only a few game modes and matchmaking functions that always look at different parts of the matchmaking pool, the Evaluator may functionally be a no-op or first-in-first-out algorithm. In complex matchmaking setups where, for example, a player can queue for multiple types of matches, the Evaluator provides the critical customizability to evaluate all available proposals and approve those that will passed to your game servers.
Large-scale concurrent matchmaking functions is a complex topic, and users who wish to do this are encouraged to engage with the [Open Match community](https://github.com/GoogleCloudPlatform/open-match#get-involved) about patterns and best practices.
### Matchmaking Functions (MMFs)
Matchmaking Functions (MMFs) are run by the Matchmaker Function Orchestrator (MMFOrc) &mdash; once per profile it sees in state storage. The MMF is run as a Job in Kubernetes, and has full access to read and write from state storage. At a high level, the encouraged pattern is to write a MMF in whatever language you are comfortable in that can do the following things:
[x] Be packaged in a (Linux) Docker container.
[x] Read/write from the Open Match state storage &mdash; Open Match ships with Redis as the default state storage.
[x] Read a profile you wrote to state storage using the Backend API.
[x] Select from the player data you wrote to state storage using the Frontend API.
[ ] Run your custom logic to try to find a match.
[x] Write the match object it creates to state storage at a specified key.
[x] Remove the players it selected from consideration by other MMFs.
[x] Notify the MMFOrc of completion.
[x] (Optional, but recommended) Export stats for metrics collection.
** Open Match offers [matchmaking logic API](#matchmaking-logic-mmlogic-api) calls for handling the checked items, as long as you are willing to format your input and output in the data schema Open Match expects (defined in the [protobuf messages](api/protobuf-spec/messages.proto)). ** You can to do this work yourself if you don't want to or can't use the data schema Open Match is looking for. However, the data formats expected by Open Match are pretty generalized and will work with most common matchmaking scenarios and game types. If you have questions about how to fit your data into the formats specified, feel free to ask us in the Slack or mailing group.
Example MMFs are provided in Golang and C#.
## Open Source Software integrations
### Structured logging
Logging for Open Match uses the [Golang logrus module](https://github.com/sirupsen/logrus) to provide structured logs. Logs are output to `stdout` in each component, as expected by Docker and Kubernetes. If you have a specific log aggregator as your final destination, we recommend you have a look at the logrus documentation as there is probably a log formatter that plays nicely with your stack.
### Instrumentation for metrics
Open Match uses [OpenCensus](https://opencensus.io/) for metrics instrumentation. The [gRPC](https://grpc.io/) integrations are built-in, and Golang redigo module integrations are incoming, but [haven't been merged into the official repo](https://github.com/opencensus-integrations/redigo/pull/1). All of the core components expose HTTP `/metrics` endpoints on the port defined in `config/matchmaker_config.json` (default: 9555) for Prometheus to scrape. If you would like to export to a different metrics aggregation platform, we suggest you have a look at the OpenCensus documentation &mdash; there may be one written for you already, and switching to it may be as simple as changing a few lines of code.
**Note:** A standard for instrumentation of MMFs is planned.
### Redis setup
By default, Open Match expects you to run Redis *somewhere*. Connection information can be put in the config file (`matchmaker_config.json`) for any Redis instance reachable from the [Kubernetes namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/). By default, Open Match sensibly runs in the Kubernetes `default` namespace. In most instances, we expect users will run a copy of Redis in a pod in Kubernetes, with a service pointing to it.
* HA configurations for Redis aren't implemented by the provided Kubernetes resource definition files, but Open Match expects the Redis service to be named `redis-sentinel`, which provides an easier path to multi-instance deployments.
## Additional examples
**Note:** These examples will be expanded on in future releases.
The following examples of how to call the APIs are provided in the repository. Both have a `Dockerfile` and `cloudbuild.yaml` files in their respective directories:
* `examples/frontendclient/main.go` acts as a client to the the Frontend API, putting a player into the queue with simulated latencies from major metropolitan cities and a couple of other matchmaking attributes. It then waits for you to manually put a value in Redis to simulate a server connection string being written using the backend API 'CreateAssignments' call, and displays that value on stdout for you to verify.
* `examples/backendclient/main.go` calls the Backend API and passes in the profile found in `backendstub/profiles/testprofile.json` to the `ListMatches` API endpoint, then continually prints the results until you exit, or there are insufficient players to make a match based on the profile..
## Usage
Documentation and usage guides on how to set up and customize Open Match.
## Precompiled container images
Once we reach a 1.0 release, we plan to produce publicly available (Linux) Docker container images of major releases in a public image registry. Until then, refer to the 'Compiling from source' section below.
## Compiling from source
All components of Open Match produce (Linux) Docker container images as artifacts, and there are included `Dockerfile`s for each. [Google Cloud Platform Cloud Build](https://cloud.google.com/cloud-build/docs/) users will also find `cloudbuild_COMPONENT.yaml` files for each component in the repository root.
All the core components for Open Match are written in Golang and use the [Dockerfile multistage builder pattern](https://docs.docker.com/develop/develop-images/multistage-build/). This pattern uses intermediate Docker containers as a Golang build environment while producing lightweight, minimized container images as final build artifacts. When the project is ready for production, we will modify the `Dockerfile`s to uncomment the last build stage. Although this pattern is great for production container images, it removes most of the utilities required to troubleshoot issues during development.
## Configuration
Currently, each component reads a local config file `matchmaker_config.json`, and all components assume they have the same configuration. To this end, there is a single centralized config file located in the `<REPO_ROOT>/config/` which is symlinked to each component's subdirectory for convenience when building locally. When `docker build`ing the component container images, the Dockerfile copies the centralized config file into the component directory.
We plan to replace this with a Kubernetes-managed config with dynamic reloading when development time allows. Pull requests are welcome!
### Guides
* [Production guide](./docs/production.md) Lots of best practices to be written here before 1.0 release. **WIP**
* [Development guide](./docs/development.md)
### Reference
* [FAQ](./docs/faq.md)
## Get involved
* [Slack channel](https://open-match.slack.com/)
* [Signup link](https://join.slack.com/t/open-match/shared_invite/enQtNDM1NjcxNTY4MTgzLWQzMzE1MGY5YmYyYWY3ZjE2MjNjZTdmYmQ1ZTQzMmNiNGViYmQyN2M4ZmVkMDY2YzZlOTUwMTYwMzI1Y2I2MjU)
* [Slack Channel](https://open-match.slack.com/) ([Signup](https://join.slack.com/t/open-match/shared_invite/enQtNDM1NjcxNTY4MTgzLWQzMzE1MGY5YmYyYWY3ZjE2MjNjZTdmYmQ1ZTQzMmNiNGViYmQyN2M4ZmVkMDY2YzZlOTUwMTYwMzI1Y2I2MjU))
* [File an Issue](https://github.com/GoogleCloudPlatform/open-match/issues/new)
* [Mailing list](https://groups.google.com/forum/#!forum/open-match-discuss)
* [Managed Service Survey](https://goo.gl/forms/cbrFTNCmy9rItSv72)
## Code of Conduct
Participation in this project comes under the [Contributor Covenant Code of Conduct](code-of-conduct.md)
## Development and Contribution
## Contributing
Please read the [contributing](CONTRIBUTING.md) guide for directions on submitting Pull Requests to Open Match.
See the [Development guide](docs/development.md) for documentation for development and building Open Match from source.
The [Release Process](docs/governance/release_process.md) documentation displays the project's upcoming release calendar and release process. (NYI)
The [Release Process](docs/governance/release_process.md) documentation displays the project's upcoming release calendar and release process.
Open Match is in active development - we would love your help in shaping its future!
## This all sounds great, but can you explain Docker and/or Kubernetes to me?
## Documentation
### Docker
- [Docker's official "Getting Started" guide](https://docs.docker.com/get-started/)
- [Katacoda's free, interactive Docker course](https://www.katacoda.com/courses/docker)
For more information on the technical underpinnings of Open Match you can refer to the [docs/](docs/) directory.
### Kubernetes
- [You should totally read this comic, and interactive tutorial](https://cloud.google.com/kubernetes-engine/kubernetes-comic/)
- [Katacoda's free, interactive Kubernetes course](https://www.katacoda.com/courses/kubernetes)
## Code of Conduct
## Licence
Participation in this project comes under the [Contributor Covenant Code of Conduct](code-of-conduct.md)
Apache 2.0
# Planned improvements
## Documentation
- [ ] “Writing your first matchmaker” getting started guide will be included in an upcoming version.
- [ ] Documentation for using the example customizable components and the `backendstub` and `frontendstub` applications to do an end-to-end (e2e) test will be written. This all works now, but needs to be written up.
- [ ] Documentation on release process and release calendar.
## State storage
- [ ] All state storage operations should be isolated from core components into the `statestorage/` modules. This is necessary precursor work to enabling Open Match state storage to use software other than Redis.
- [ ] The Redis deployment should have an example HA configuration using HAProxy
- [ ] Redis watch should be unified to watch a hash and stream updates. The code for this is written and validated but not committed yet. We don't want to support two redis watcher code paths, so the backend watch of the match object should be switched to unify the way the frontend and backend watch keys. The backend part of this is in but the frontend part is in another branch and will be committed later.
## Instrumentation / Metrics / Analytics
- [ ] Instrumentation of MMFs is in the planning stages. Since MMFs are by design meant to be completely customizable (to the point of allowing any process that can be packaged in a Docker container), metrics/stats will need to have an expected format and formalized outgoing pathway. Currently the thought is that it might be that the metrics should be written to a particular key in statestorage in a format compatible with opencensus, and will be collected, aggreggated, and exported to Prometheus using another process.
- [ ] [OpenCensus tracing](https://opencensus.io/core-concepts/tracing/) will be implemented in an upcoming version.
- [ ] Read logrus logging configuration from matchmaker_config.json.
## Security
- [ ] The Kubernetes service account used by the MMFOrc should be updated to have min required permissions.
## Kubernetes
- [ ] Autoscaling isn't turned on for the Frontend or Backend API Kubernetes deployments by default.
- [ ] A [Helm](https://helm.sh/) chart to stand up Open Match will be provided in an upcoming version. For now just use the [installation YAMLs](./install/yaml).
- [ ] Player/Group records generated when a client enters the matchmaking pool need to be removed after a certain amount of time with no activity. When using Redis, this will be implemented as a expiration on the player record.
## CI / CD / Build
- [ ] We plan to host 'official' docker images for all release versions of the core components in publicly available docker registries soon.
- [ ] CI/CD for this repo and the associated status tags are planned.
- [ ] Golang unit tests will be shipped in an upcoming version.
- [ ] A full load-testing and e2e testing suite will be included in an upcoming version.
## Will not Implement
- [X] Defining multiple images inside a profile for the purposes of experimentation adds another layer of complexity into profiles that can instead be handled outside of open match with custom match functions in collaboration with a director (thing that calls backend to schedule matchmaking)
## Disclaimer
This software is currently alpha, and subject to change. Although Open Match has already been used to run [production workloads within Google](https://cloud.google.com/blog/topics/inside-google-cloud/no-tricks-just-treats-globally-scaling-the-halloween-multiplayer-doodle-with-open-match-on-google-cloud), but it's still early days on the way to our final goal. There's plenty left to write and we welcome contributions. **We strongly encourage you to engage with the community through the [Slack or Mailing lists](#support) if you're considering using Open Match in production before the 1.0 release, as the documentation is likely to lag behind the latest version a bit while we focus on getting out of alpha/beta as soon as possible.**

@ -1,32 +0,0 @@
# Release history
##v0.2.0 RC2 (alpha)
This is a pretty large update. Custom MMFs or evaluators from 0.1.0 may need some tweaking to work with this version. Some Backend API function arguments have changed. Please join the [Slack channel](https://open-match.slack.com/) if you need help ([Signup link](https://join.slack.com/t/open-match/shared_invite/enQtNDM1NjcxNTY4MTgzLWQzMzE1MGY5YmYyYWY3ZjE2MjNjZTdmYmQ1ZTQzMmNiNGViYmQyN2M4ZmVkMDY2YzZlOTUwMTYwMzI1Y2I2MjU))!
v0.2.0 focused on adding additional functionality to Backend API calls and on **reducing the amount of boilerplate code required to make a custom Matchmaking Function**. For this, a new internal API for use by MMFs called the [Matchmaking Logic API (MMLogic API)](README.md#matchmaking-logic-mmlogic-api) has been added. Many of the core components and examples had to be updated to use the new Backend API arguments and the modules to support them, so we recommend you rebuild and redeploy all the components to use v0.2.0.
### Release notes
- MMLogic API is now available. Deploy it to kubernetes using the [appropriate json file]() and check out the [gRPC API specification](api/protobuf-spec/mmlogic.proto) to see how to use it. To write a client against this API, you'll need to compile the protobuf files to your language of choice. There is an associated cloudbuild.yaml file and Dockerfile for it in the root directory.
- When using the MMLogic API to filter players into pools, it will attempt to report back the number of players that matched the filters and how long the filters took to query state storage.
- An [example MMF](examples/functions/python3/mmlogic-simple/harness.py) using it has been written in Python3. There is an associated cloudbuild.yaml file and Dockerfile for it in the root directory. By default the [example backend client](examples/backendclient/main.go) is now configured to use this MMF, so make sure you have it avaiable before you try to run the latest backend client.
- An [example MMF](examples/functions/php/mmlogic-simple/harness.py) using it has been contributed by Ilya Hrankouski in PHP (thanks!). - The API specs have been split into separate files per API and the protobuf messages are in a separate file. Things were renamed slightly as a result, and you will need to update your API clients. The Frontend API hasn't had it's messages moved to the shared messages file yet, but this will happen in an upcoming version.
- The [example golang MMF](examples/functions/golang/manual-simple/) has been updated to use the latest data schemas for MatchObjects, and renamed to `manual-simple` to denote that it is manually manipulating Redis, not using the MMLogic API.
- The API specs have been split into separate files per API and the protobuf messages are in a separate file. Things were renamed slightly as a result, and you will need to update your API clients. The Frontend API hasn't had it's messages moved to the shared messages file yet, but this will happen in an upcoming version.
- The message model for using the Backend API has changed slightly - for calls that make MatchObjects, the expectation is that you will provide a MatchObject with a few fields populated, and it will then be shuttled along through state storage to your MMF and back out again, with various processes 'filling in the blanks' of your MatchObject, which is then returned to your code calling the Backend API. Read the[gRPC API specification](api/protobuf-spec/backend.proto) for more information.
- As part of this, compiled protobuf golang modules now live in the [`internal/pb`](internal/pb) directory. There's a handy [bash script](api/protoc-go.sh) for compiling them from the `api/protobuf-spec` directory into this new `internal/pb` directory for development in your local golang environment if you need it.
- As part of this Backend API message shift and the advent of the MMLogic API, 'player pools' and 'rosters' are now first-class data structures in MatchObjects for those who wish to use them. You can ignore them if you like, but if you want to use some of the MMLogic API calls to automate tasks for you - things like filtering a pool of players according attributes or adding all the players in your rosters to the ignorelist so other MMFs don't try to grab them - you'll need to put your data into the [protobuf messages](api/protobuf-spec/messages.proto) so Open Match knows how to read them. The sample backend client [test profile JSON](examples/backendclient/profiles/testprofile.json)has been updated to use this format if you want to see an example.
- Rosters were formerly space-delimited lists of player IDs. They are now first-class repeated protobuf message fields in the [Roster message format](api/protobuf-spec/messages.proto). That means that in most languages, you can access the roster as a list of players using your native language data structures (more info can be found in the [guide for using protocol buffers in your langauge of choice](https://developers.google.com/protocol-buffers/docs/reference/overview)). If you don't care about the new fields or the new functionality, you can just leave all the other fields but the player ID unset.
- Open Match is transitioning to using [protocol buffer messages](https://developers.google.com/protocol-buffers/) as its internal data format. There is now a Redis state storage [golang module](internal/statestorage/redis/redispb/) for marshaling and unmarshaling MatchObject messages to and from Redis. It isn't very clean code right now but will get worked on for the next couple releases.
- Ignorelists now exist, and have a Redis state storage [golang module](internal/statestorage/redis/ignorelist/) for CRUD access. Currently three ignorelists are defined in the [config file](config/matchmaker_config.json) with their respective parameters. These are implemented as [Sorted Sets in Redis](https://redis.io/commands#sorted_set).
- For those who only want to stand up Open Match and aren't interested in individually tweaking the required kubernetes resources, there are now [three YAML files](install/yaml) that can be used to install Redis, install Open Match, and (optionally) install Prometheus. You'll still need the `sed` [instructions from the Developer Guide](docs/development.md#running-open-match-in-a-development-environment) to substitute in the name of your Docker container registry.
- A super-simple module has been created for doing instersections, unions, and differences of lists of player IDs. It lives in `internal/set/set.go`.
### Roadmap
- It has become clear from talking to multiple users that the software they write to talk to the Backend API needs a name. 'Backend API Client' is technically correct, but given how many APIs are in Open Match and the overwhelming use of 'Client' to refer to a Game Client in the industry, we're currently calling this a 'Director', as its primary purpose is to 'direct' which profiles are sent to the backend, and 'direct' the resulting MatchObjects to game servers. Further discussion / suggestions are welcome.
- We'll be entering the design stage on longer-running MMFs before the end of the year. We'll get a proposal together and on the github repo as a request for comments, so please keep your eye out for that.
- Match profiles providing multiple MMFs to run isn't planned anymore. Just send multiple copies of the profile with different MMFs specified via the backendapi.
- Redis Sentinel will likely not be supported. Instead, replicated instances and HAProxy may be the HA solution of choice. There's an [outstanding issue to investigate and implement](https://github.com/GoogleCloudPlatform/open-match/issues/41) if it fills our needs, feel free to contribute!
## v0.1.0 (alpha)
Initial release.

@ -4,12 +4,9 @@ This directory contains the API specification files for Open Match. API documena
* [Protobuf .proto files for all APIs](./protobuf-spec/)
These proto files are copied to the container image during `docker build` for the Open Match core components. The `Dockerfiles` handle the compilation for you transparently, and copy the resulting `SPEC.pb.go` files to the appropriate place in your final container image.
References:
* [gRPC](https://grpc.io/)
* [Language Guide (proto3)](https://developers.google.com/protocol-buffers/docs/proto3)
Manual gRPC compilation commmand, from the directory containing the proto:
```protoc -I . ./<filename>.proto --go_out=plugins=grpc:.```
If you want to regenerate the golang gRPC modules (for local Open Match core component development, for example), the `protoc_go.sh` file in this directory may be of use to you!

@ -1,17 +1,27 @@
** REST compatibility
Follow the guidelines at https://cloud.google.com/endpoints/docs/grpc/transcoding
to keep the gRPC service definitions friendly to REST transcoding. An excerpt:
## REST compatibility
Follow the guidelines at https://cloud.google.com/endpoints/docs/grpc/transcoding to keep the gRPC service definitions friendly to REST transcoding. An excerpt:
"Transcoding involves mapping HTTP/JSON requests and their parameters to gRPC
methods and their parameters and return types (we'll look at exactly how you
do this in the following sections). Because of this, while it's possible to
map an HTTP/JSON request to any arbitrary API method, it's simplest and most
intuitive to do so if the gRPC API itself is structured in a
resource-oriented way, just like a traditional HTTP REST API. In other
words, the API service should be designed so that it uses a small number of
standard methods (corresponding to HTTP verbs like GET, PUT, and so on) that
operate on the service's resources (and collections of resources, which are
themselves a type of resource).
These standard methods are List, Get, Create, Update, and Delete."
"Transcoding involves mapping HTTP/JSON requests and their parameters to gRPC methods and their parameters and return types (we'll look at exactly how you do this in the following sections). Because of this, while it's possible to map an HTTP/JSON request to any arbitrary API method, it's simplest and most intuitive to do so if the gRPC API itself is structured in a resource-oriented way, just like a traditional HTTP REST API. In other words, the API service should be designed so that it uses a small number of standard methods (corresponding to HTTP verbs like GET, PUT, and so on) that operate on the service's resources (and collections of resources, which are themselves a type of resource). These standard methods are List, Get, Create, Update, and Delete."
It is for these reasons we don't have gRPC calls that support bi-directional streaming in Open Match.
## REST API Usage
Open Match gateway proxy transcodes any REST calls to its underlying gRPC service. Follow the [examples](https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#httprule) for further details.
A typical REST call to Open Match backend's `CreateAssignments` service via HTTP POST request
```
/v1/backend/assignments/123? \
assignment.rosters.name=foo&assignment.rosters.players.id=1&assignment.rosters.players.id=2
```
is equivalent to
```go
CreateAssignmentsRequest(
Assignments(
name: '123',
rosters: [
Roster(name: 'foo', [Player(id: 1), Player(id: 2)])
]
)
)
```

@ -1,9 +1,75 @@
// Copyright 2018 Google LLC
//
// 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.
syntax = 'proto3';
package api;
option go_package = "github.com/GoogleCloudPlatform/open-match/internal/pb";
option go_package = "internal/pb";
// The protobuf messages sent in the gRPC calls are defined 'messages.proto'.
import 'api/protobuf-spec/messages.proto';
import 'google/api/annotations.proto';
message MmfConfig {
enum Type {
GRPC = 0;
REST = 1; // REST support will be added in future.
}
string name = 1; // Developer-chosen, human-readable string. (Optional)
string host = 2; // Host or DNS name for service providing this MMF. Must be resolve-able by the backend API.
int32 port = 3; // Port number for service providing this MMF.
Type type = 4; // Type of MMF call
}
message CreateMatchRequest {
messages.MatchObject match = 1;
MmfConfig mmfcfg = 2;
}
message CreateMatchResponse {
messages.MatchObject match = 1;
}
message ListMatchesRequest {
messages.MatchObject match = 1;
MmfConfig mmfcfg = 2;
}
message ListMatchesResponse {
messages.MatchObject match = 1;
}
message DeleteMatchRequest {
messages.MatchObject match = 1;
}
message DeleteMatchResponse {
}
message CreateAssignmentsRequest {
messages.Assignments assignment = 1;
}
message CreateAssignmentsResponse {
}
message DeleteAssignmentsRequest {
messages.Roster roster = 1;
}
message DeleteAssignmentsResponse {
}
service Backend {
// Calls to ask the matchmaker to run a matchmaking function.
@ -20,37 +86,65 @@ service Backend {
// - error. Empty if no error was encountered
// - rosters, if you choose to fill them in your MMF. (Recommended)
// - pools, if you used the MMLogicAPI in your MMF. (Recommended, and provides stats)
rpc CreateMatch(messages.MatchObject) returns (messages.MatchObject) {}
// Continually run MMF and stream matchobjects that fit this profile until
// client closes the connection. Same inputs/outputs as CreateMatch.
rpc ListMatches(messages.MatchObject) returns (stream messages.MatchObject) {}
rpc CreateMatch(CreateMatchRequest) returns (CreateMatchResponse) {
option (google.api.http) = {
put: "/v1/backend/matches"
body: "*"
};
}
// Continually run MMF and stream MatchObjects that fit this profile until
// the backend client closes the connection. Same inputs/outputs as CreateMatch.
rpc ListMatches(ListMatchesRequest) returns (stream ListMatchesResponse) {
option (google.api.http).get = "/v1/backend/matches/{match.id}/{match.properties}";
}
// Delete a matchobject from state storage manually. (Matchobjects in state
// storage will also automatically expire after a while)
// INPUT: MatchObject message with the 'id' field populated.
// Delete a MatchObject from state storage manually. (MatchObjects in state
// storage will also automatically expire after a while, defined in the config)
// INPUT: MatchObject message with the 'id' field populated.
// (All other fields are ignored.)
rpc DeleteMatch(messages.MatchObject) returns (messages.Result) {}
rpc DeleteMatch(DeleteMatchRequest) returns (DeleteMatchResponse) {
option (google.api.http) = {
delete: "/v1/backend/matches"
body: "*"
additional_bindings {
delete: "/v1/backend/matches/{match.id}"
}
};
}
// Call fors communication of connection info to players.
// Calls for communication of connection info to players.
// Write the connection info for the list of players in the
// Assignments.messages.Rosters to state storage. The FrontendAPI is
// Assignments.messages.Rosters to state storage. The Frontend API is
// responsible for sending anything sent here to the game clients.
// Sending a player to this function kicks off a process that removes
// the player from future matchmaking functions by adding them to the
// the player from future matchmaking functions by adding them to the
// 'deindexed' player list and then deleting their player ID from state storage
// indexes.
// INPUT: Assignments message with these fields populated:
// - connection_info, anything you write to this string is sent to Frontend API
// - assignment, anything you write to this string is sent to Frontend API
// - rosters. You can send any number of rosters, containing any number of
// player messages. All players from all rosters will be sent the connection_info.
// The only field in the Player object that is used by CreateAssignments is
// the id field. All others are silently ignored.
rpc CreateAssignments(messages.Assignments) returns (messages.Result) {}
// Remove DGS connection info from state storage for players.
// INPUT: Roster message with the 'players' field populated.
// The only field in the Player object that is used by
// player messages. All players from all rosters will be sent the assignment.
// The only field in the Roster's Player messages used by CreateAssignments is
// the id field. All other fields in the Player messages are silently ignored.
rpc CreateAssignments(CreateAssignmentsRequest) returns (CreateAssignmentsResponse) {
option (google.api.http)= {
put: "/v1/backend/assignments"
body: "*"
};
}
// Remove DGS connection info from state storage for players.
// INPUT: Roster message with the 'players' field populated.
// The only field in the Roster's Player messages used by
// DeleteAssignments is the 'id' field. All others are silently ignored. If
// you need to delete multiple rosters, make multiple calls.
rpc DeleteAssignments(messages.Roster) returns (messages.Result) {}
rpc DeleteAssignments(DeleteAssignmentsRequest) returns (DeleteAssignmentsResponse) {
option (google.api.http) = {
delete: "/v1/backend/assignments"
body: "*"
additional_bindings {
delete: "/v1/backend/assignments"
}
};
}
}

@ -0,0 +1,535 @@
{
"swagger": "2.0",
"info": {
"title": "api/protobuf-spec/backend.proto",
"version": "version not set"
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/backend/assignments": {
"delete": {
"summary": "Remove DGS connection info from state storage for players.\nINPUT: Roster message with the 'players' field populated.\n The only field in the Roster's Player messages used by\n DeleteAssignments is the 'id' field. All others are silently ignored. If\n you need to delete multiple rosters, make multiple calls.",
"operationId": "DeleteAssignments2",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiDeleteAssignmentsResponse"
}
}
},
"parameters": [
{
"name": "roster.name",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"Backend"
]
},
"put": {
"summary": "Write the connection info for the list of players in the\nAssignments.messages.Rosters to state storage. The Frontend API is\nresponsible for sending anything sent here to the game clients.\nSending a player to this function kicks off a process that removes\nthe player from future matchmaking functions by adding them to the\n'deindexed' player list and then deleting their player ID from state storage\nindexes.\nINPUT: Assignments message with these fields populated:\n - assignment, anything you write to this string is sent to Frontend API\n - rosters. You can send any number of rosters, containing any number of\n player messages. All players from all rosters will be sent the assignment.\n The only field in the Roster's Player messages used by CreateAssignments is\n the id field. All other fields in the Player messages are silently ignored.",
"operationId": "CreateAssignments",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiCreateAssignmentsResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiCreateAssignmentsRequest"
}
}
],
"tags": [
"Backend"
]
}
},
"/v1/backend/matches": {
"delete": {
"summary": "Delete a MatchObject from state storage manually. (MatchObjects in state\nstorage will also automatically expire after a while, defined in the config)\nINPUT: MatchObject message with the 'id' field populated.\n(All other fields are ignored.)",
"operationId": "DeleteMatch",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiDeleteMatchResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiDeleteMatchRequest"
}
}
],
"tags": [
"Backend"
]
},
"put": {
"summary": "Run MMF once. Return a matchobject that fits this profile.\nINPUT: MatchObject message with these fields populated:\n - id\n - properties\n - [optional] roster, any fields you fill are available to your MMF.\n - [optional] pools, any fields you fill are available to your MMF.\nOUTPUT: MatchObject message with these fields populated:\n - id\n - properties\n - error. Empty if no error was encountered\n - rosters, if you choose to fill them in your MMF. (Recommended)\n - pools, if you used the MMLogicAPI in your MMF. (Recommended, and provides stats)",
"operationId": "CreateMatch",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiCreateMatchResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiCreateMatchRequest"
}
}
],
"tags": [
"Backend"
]
}
},
"/v1/backend/matches/{match.id}": {
"delete": {
"summary": "Delete a MatchObject from state storage manually. (MatchObjects in state\nstorage will also automatically expire after a while, defined in the config)\nINPUT: MatchObject message with the 'id' field populated.\n(All other fields are ignored.)",
"operationId": "DeleteMatch2",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiDeleteMatchResponse"
}
}
},
"parameters": [
{
"name": "match.id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "match.properties",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "match.error",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "match.status",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"Backend"
]
}
},
"/v1/backend/matches/{match.id}/{match.properties}": {
"get": {
"summary": "Continually run MMF and stream MatchObjects that fit this profile until\nthe backend client closes the connection. Same inputs/outputs as CreateMatch.",
"operationId": "ListMatches",
"responses": {
"200": {
"description": "A successful response.(streaming responses)",
"schema": {
"$ref": "#/x-stream-definitions/apiListMatchesResponse"
}
}
},
"parameters": [
{
"name": "match.id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "match.properties",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "match.error",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "match.status",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "mmfcfg.name",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "mmfcfg.host",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "mmfcfg.port",
"in": "query",
"required": false,
"type": "integer",
"format": "int32"
},
{
"name": "mmfcfg.type",
"in": "query",
"required": false,
"type": "string",
"enum": [
"GRPC",
"REST"
],
"default": "GRPC"
}
],
"tags": [
"Backend"
]
}
}
},
"definitions": {
"PlayerAttribute": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string",
"format": "int64"
}
}
},
"apiCreateAssignmentsRequest": {
"type": "object",
"properties": {
"assignment": {
"$ref": "#/definitions/messagesAssignments"
}
}
},
"apiCreateAssignmentsResponse": {
"type": "object"
},
"apiCreateMatchRequest": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
},
"mmfcfg": {
"$ref": "#/definitions/apiMmfConfig"
}
}
},
"apiCreateMatchResponse": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
}
}
},
"apiDeleteAssignmentsRequest": {
"type": "object",
"properties": {
"roster": {
"$ref": "#/definitions/messagesRoster"
}
}
},
"apiDeleteAssignmentsResponse": {
"type": "object"
},
"apiDeleteMatchRequest": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
}
}
},
"apiDeleteMatchResponse": {
"type": "object"
},
"apiListMatchesResponse": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
}
}
},
"apiMmfConfig": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"host": {
"type": "string"
},
"port": {
"type": "integer",
"format": "int32"
},
"type": {
"$ref": "#/definitions/apiMmfConfigType"
}
}
},
"apiMmfConfigType": {
"type": "string",
"enum": [
"GRPC",
"REST"
],
"default": "GRPC"
},
"messagesAssignments": {
"type": "object",
"properties": {
"rosters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesRoster"
}
},
"assignment": {
"type": "string"
}
}
},
"messagesFilter": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"attribute": {
"type": "string"
},
"maxv": {
"type": "string",
"format": "int64"
},
"minv": {
"type": "string",
"format": "int64"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "A 'hard' filter to apply to the player pool."
},
"messagesMatchObject": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"error": {
"type": "string"
},
"rosters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesRoster"
}
},
"pools": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayerPool"
}
},
"status": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"MatchObjects\".\nIn order to request a match using the Backend API, your backend code should generate\na new MatchObject with an ID and properties filled in (for more details about valid\nvalues for these fields, see the documentation). Open Match then sends the Match\nObject through to your matchmaking function, where you add players to 'rosters' and\nstore any schemaless data you wish in the 'properties' field. The MatchObject\nis then sent, populated, out through the Backend API to your backend code.\n\nMatchObjects contain a number of fields, but many gRPC calls that take a\nMatchObject as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayer": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"pool": {
"type": "string"
},
"attributes": {
"type": "array",
"items": {
"$ref": "#/definitions/PlayerAttribute"
}
},
"assignment": {
"type": "string"
},
"status": {
"type": "string"
},
"error": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"Players\".\nIn order to enter matchmaking using the Frontend API, your client code should generate\na consistent (same result for each client every time they launch) with an ID and\nproperties filled in (for more details about valid values for these fields,\nsee the documentation).\nPlayers contain a number of fields, but the gRPC calls that take a\nPlayer as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayerPool": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesFilter"
}
},
"roster": {
"$ref": "#/definitions/messagesRoster"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "PlayerPools are defined by a set of 'hard' filters, and can be filled in\nwith the players that match those filters.\n\nPlayerPools contain a number of fields, but many gRPC calls that take a\nPlayerPool as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesRoster": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"players": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayer"
}
}
},
"description": "Data structure to hold a list of players in a match."
},
"messagesStats": {
"type": "object",
"properties": {
"count": {
"type": "string",
"format": "int64"
},
"elapsed": {
"type": "number",
"format": "double"
}
},
"title": "Holds statistics"
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string"
},
"value": {
"type": "string",
"format": "byte"
}
}
},
"runtimeStreamError": {
"type": "object",
"properties": {
"grpc_code": {
"type": "integer",
"format": "int32"
},
"http_code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"http_status": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
},
"x-stream-definitions": {
"apiListMatchesResponse": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/apiListMatchesResponse"
},
"error": {
"$ref": "#/definitions/runtimeStreamError"
}
},
"title": "Stream result of apiListMatchesResponse"
}
}
}

@ -1,23 +1,112 @@
// TODO: In a future version, these messages will be moved/merged with those in om_messages.proto
// Copyright 2018 Google LLC
//
// 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.
syntax = 'proto3';
package api;
option go_package = "github.com/GoogleCloudPlatform/open-match/internal/pb";
option go_package = "internal/pb";
import 'api/protobuf-spec/messages.proto';
import 'google/api/annotations.proto';
message CreatePlayerRequest {
messages.Player player = 1;
}
message CreatePlayerResponse {
}
message DeletePlayerRequest {
messages.Player player = 1;
}
message DeletePlayerResponse {
}
message GetUpdatesRequest {
messages.Player player = 1;
}
message GetUpdatesResponse {
messages.Player player = 1;
}
service Frontend {
rpc CreateRequest(Group) returns (messages.Result) {}
rpc DeleteRequest(Group) returns (messages.Result) {}
rpc GetAssignment(PlayerId) returns (messages.ConnectionInfo) {}
rpc DeleteAssignment(PlayerId) returns (messages.Result) {}
}
// Call to start matchmaking for a player
// Data structure for a group of players to pass to the matchmaking function.
// Obviously, the group can be a group of one!
message Group{
string id = 1; // By convention, string of space-delimited playerIDs
string properties = 2; // By convention, a JSON-encoded string
}
// CreatePlayer will put the player in state storage, and then look
// through the 'properties' field for the attributes you have defined as
// indices your matchmaker config. If the attributes exist and are valid
// integers, they will be indexed.
// INPUT: Player message with these fields populated:
// - id
// - properties
// OUTPUT: Result message denoting success or failure (and an error if
// necessary)
rpc CreatePlayer(CreatePlayerRequest) returns (CreatePlayerResponse) {
option (google.api.http) = {
put: "/v1/frontend/players"
body: "*"
};
}
message PlayerId {
string id = 1; // By convention, a UUID
// Call to stop matchmaking for a player
// DeletePlayer removes the player from state storage by doing the
// following:
// 1) Delete player from configured indices. This effectively removes the
// player from matchmaking when using recommended MMF patterns.
// Everything after this is just cleanup to save stage storage space.
// 2) 'Lazily' delete the player's state storage record. This is kicked
// off in the background and may take some time to complete.
// 2) 'Lazily' delete the player's metadata indicies (like, the timestamp when
// they called CreatePlayer, and the last time the record was accessed). This
// is also kicked off in the background and may take some time to complete.
// INPUT: Player message with the 'id' field populated.
// OUTPUT: Result message denoting success or failure (and an error if
// necessary)
rpc DeletePlayer(DeletePlayerRequest) returns (DeletePlayerResponse) {
option (google.api.http).delete = "/v1/frontend/players/{player.id}";
}
// Calls to access matchmaking results for a player
// GetUpdates streams matchmaking results from Open Match for the
// provided player ID.
// INPUT: Player message with the 'id' field populated.
// OUTPUT: a stream of player objects with one or more of the following
// fields populated, if an update to that field is seen in state storage:
// - 'assignment': string that usually contains game server connection information.
// - 'status': string to communicate current matchmaking status to the client.
// - 'error': string to pass along error information to the client.
//
// During normal operation, the expectation is that the 'assignment' field
// will be updated by a Backend process calling the 'CreateAssignments' Backend API
// endpoint. 'Status' and 'Error' are free for developers to use as they see fit.
// Even if you had multiple players enter a matchmaking request as a group, the
// Backend API 'CreateAssignments' call will write the results to state
// storage separately under each player's ID. OM expects you to make all game
// clients 'GetUpdates' with their own ID from the Frontend API to get
// their results.
//
// NOTE: This call generates a small amount of load on the Frontend API and state
// storage while watching the player record for updates. You are expected
// to close the stream from your client after receiving your matchmaking
// results (or a reasonable timeout), or you will continue to
// generate load on OM until you do!
// NOTE: Just bear in mind that every update will send egress traffic from
// Open Match to game clients! Frugality is recommended.
rpc GetUpdates(GetUpdatesRequest) returns (stream GetUpdatesResponse) {
option (google.api.http).get = "/v1/frontend/players/{player.id}";
}
}

@ -0,0 +1,272 @@
{
"swagger": "2.0",
"info": {
"title": "api/protobuf-spec/frontend.proto",
"version": "version not set"
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/frontend/players": {
"put": {
"summary": "CreatePlayer will put the player in state storage, and then look\nthrough the 'properties' field for the attributes you have defined as\nindices your matchmaker config. If the attributes exist and are valid\nintegers, they will be indexed.\nINPUT: Player message with these fields populated:\n - id\n - properties\nOUTPUT: Result message denoting success or failure (and an error if\nnecessary)",
"operationId": "CreatePlayer",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiCreatePlayerResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiCreatePlayerRequest"
}
}
],
"tags": [
"Frontend"
]
}
},
"/v1/frontend/players/{player.id}": {
"get": {
"summary": "GetUpdates streams matchmaking results from Open Match for the\nprovided player ID.\nINPUT: Player message with the 'id' field populated.\nOUTPUT: a stream of player objects with one or more of the following\nfields populated, if an update to that field is seen in state storage:\n - 'assignment': string that usually contains game server connection information.\n - 'status': string to communicate current matchmaking status to the client.\n - 'error': string to pass along error information to the client.",
"description": "During normal operation, the expectation is that the 'assignment' field\nwill be updated by a Backend process calling the 'CreateAssignments' Backend API\nendpoint. 'Status' and 'Error' are free for developers to use as they see fit. \nEven if you had multiple players enter a matchmaking request as a group, the\nBackend API 'CreateAssignments' call will write the results to state\nstorage separately under each player's ID. OM expects you to make all game\nclients 'GetUpdates' with their own ID from the Frontend API to get\ntheir results.\n\nNOTE: This call generates a small amount of load on the Frontend API and state\n storage while watching the player record for updates. You are expected\n to close the stream from your client after receiving your matchmaking\n results (or a reasonable timeout), or you will continue to\n generate load on OM until you do!\nNOTE: Just bear in mind that every update will send egress traffic from\n Open Match to game clients! Frugality is recommended.",
"operationId": "GetUpdates",
"responses": {
"200": {
"description": "A successful response.(streaming responses)",
"schema": {
"$ref": "#/x-stream-definitions/apiGetUpdatesResponse"
}
}
},
"parameters": [
{
"name": "player.id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "player.properties",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.pool",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.assignment",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.status",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.error",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"Frontend"
]
},
"delete": {
"summary": "DeletePlayer removes the player from state storage by doing the\nfollowing:\n 1) Delete player from configured indices. This effectively removes the\n player from matchmaking when using recommended MMF patterns.\n Everything after this is just cleanup to save stage storage space.\n 2) 'Lazily' delete the player's state storage record. This is kicked\n off in the background and may take some time to complete.\n 2) 'Lazily' delete the player's metadata indicies (like, the timestamp when \n they called CreatePlayer, and the last time the record was accessed). This \n is also kicked off in the background and may take some time to complete.\nINPUT: Player message with the 'id' field populated.\nOUTPUT: Result message denoting success or failure (and an error if\nnecessary)",
"operationId": "DeletePlayer",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiDeletePlayerResponse"
}
}
},
"parameters": [
{
"name": "player.id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "player.properties",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.pool",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.assignment",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.status",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.error",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"Frontend"
]
}
}
},
"definitions": {
"PlayerAttribute": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string",
"format": "int64"
}
}
},
"apiCreatePlayerRequest": {
"type": "object",
"properties": {
"player": {
"$ref": "#/definitions/messagesPlayer"
}
}
},
"apiCreatePlayerResponse": {
"type": "object"
},
"apiDeletePlayerResponse": {
"type": "object"
},
"apiGetUpdatesResponse": {
"type": "object",
"properties": {
"player": {
"$ref": "#/definitions/messagesPlayer"
}
}
},
"messagesPlayer": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"pool": {
"type": "string"
},
"attributes": {
"type": "array",
"items": {
"$ref": "#/definitions/PlayerAttribute"
}
},
"assignment": {
"type": "string"
},
"status": {
"type": "string"
},
"error": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"Players\".\nIn order to enter matchmaking using the Frontend API, your client code should generate\na consistent (same result for each client every time they launch) with an ID and\nproperties filled in (for more details about valid values for these fields,\nsee the documentation).\nPlayers contain a number of fields, but the gRPC calls that take a\nPlayer as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string"
},
"value": {
"type": "string",
"format": "byte"
}
}
},
"runtimeStreamError": {
"type": "object",
"properties": {
"grpc_code": {
"type": "integer",
"format": "int32"
},
"http_code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"http_status": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
},
"x-stream-definitions": {
"apiGetUpdatesResponse": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/apiGetUpdatesResponse"
},
"error": {
"$ref": "#/definitions/runtimeStreamError"
}
},
"title": "Stream result of apiGetUpdatesResponse"
}
}
}

@ -0,0 +1,48 @@
// Copyright 2018 Google LLC
//
// 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.
syntax = 'proto3';
package api;
option go_package = "internal/pb";
// The protobuf messages sent in the gRPC calls are defined 'messages.proto'.
import 'api/protobuf-spec/messages.proto';
import 'google/api/annotations.proto';
// Request message sent to the MMF.
message RunRequest {
string profile_id = 1; // Developer-chosen profile name, state storage key for the match object.
string proposal_id = 2; // The ID against which, the generated proposal should be stored.
string result_id = 3; // Final result ID. MMF needs to know this in case of errors where proposal generation can be shortcircuited.
messages.MatchObject match_object = 4; // The match object containing the details of the match to be generated.
string timestamp = 5;
}
message RunResponse {
}
// The MMF proto defines the API for running MMFs as long-lived, 'serving'
// functions inside of the kubernetes cluster.
service MatchFunction {
// The assumption is that there will be one service for each MMF that is
// being served. Build your MMF in the appropriate serving harness, deploy it
// to the K8s cluster with a unique service name, then connect to that service
// and call 'Run()' to execute the fuction.
rpc Run(RunRequest) returns (RunResponse) {
option (google.api.http) = {
put: "/v1/function"
body: "*"
};
}
}

@ -0,0 +1,217 @@
{
"swagger": "2.0",
"info": {
"title": "api/protobuf-spec/matchfunction.proto",
"version": "version not set"
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/function": {
"put": {
"summary": "The assumption is that there will be one service for each MMF that is\nbeing served. Build your MMF in the appropriate serving harness, deploy it\nto the K8s cluster with a unique service name, then connect to that service\nand call 'Run()' to execute the fuction.",
"operationId": "Run",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiRunResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiRunRequest"
}
}
],
"tags": [
"MatchFunction"
]
}
}
},
"definitions": {
"PlayerAttribute": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string",
"format": "int64"
}
}
},
"apiRunRequest": {
"type": "object",
"properties": {
"profile_id": {
"type": "string"
},
"proposal_id": {
"type": "string"
},
"result_id": {
"type": "string"
},
"match_object": {
"$ref": "#/definitions/messagesMatchObject"
},
"timestamp": {
"type": "string"
}
},
"description": "Request message sent to the MMF."
},
"apiRunResponse": {
"type": "object"
},
"messagesFilter": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"attribute": {
"type": "string"
},
"maxv": {
"type": "string",
"format": "int64"
},
"minv": {
"type": "string",
"format": "int64"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "A 'hard' filter to apply to the player pool."
},
"messagesMatchObject": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"error": {
"type": "string"
},
"rosters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesRoster"
}
},
"pools": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayerPool"
}
},
"status": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"MatchObjects\".\nIn order to request a match using the Backend API, your backend code should generate\na new MatchObject with an ID and properties filled in (for more details about valid\nvalues for these fields, see the documentation). Open Match then sends the Match\nObject through to your matchmaking function, where you add players to 'rosters' and\nstore any schemaless data you wish in the 'properties' field. The MatchObject\nis then sent, populated, out through the Backend API to your backend code.\n\nMatchObjects contain a number of fields, but many gRPC calls that take a\nMatchObject as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayer": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"pool": {
"type": "string"
},
"attributes": {
"type": "array",
"items": {
"$ref": "#/definitions/PlayerAttribute"
}
},
"assignment": {
"type": "string"
},
"status": {
"type": "string"
},
"error": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"Players\".\nIn order to enter matchmaking using the Frontend API, your client code should generate\na consistent (same result for each client every time they launch) with an ID and\nproperties filled in (for more details about valid values for these fields,\nsee the documentation).\nPlayers contain a number of fields, but the gRPC calls that take a\nPlayer as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayerPool": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesFilter"
}
},
"roster": {
"$ref": "#/definitions/messagesRoster"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "PlayerPools are defined by a set of 'hard' filters, and can be filled in\nwith the players that match those filters.\n\nPlayerPools contain a number of fields, but many gRPC calls that take a\nPlayerPool as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesRoster": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"players": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayer"
}
}
},
"description": "Data structure to hold a list of players in a match."
},
"messagesStats": {
"type": "object",
"properties": {
"count": {
"type": "string",
"format": "int64"
},
"elapsed": {
"type": "number",
"format": "double"
}
},
"title": "Holds statistics"
}
}
}

@ -1,6 +1,20 @@
// Copyright 2018 Google LLC
//
// 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.
syntax = 'proto3';
package messages;
option go_package = "github.com/GoogleCloudPlatform/open-match/internal/pb";
option go_package = "internal/pb";
// Open Match's internal representation and wire protocol format for "MatchObjects".
// In order to request a match using the Backend API, your backend code should generate
@ -9,37 +23,38 @@ option go_package = "github.com/GoogleCloudPlatform/open-match/internal/pb";
// Object through to your matchmaking function, where you add players to 'rosters' and
// store any schemaless data you wish in the 'properties' field. The MatchObject
// is then sent, populated, out through the Backend API to your backend code.
//
//
// MatchObjects contain a number of fields, but many gRPC calls that take a
// MatchObject as input only require a few of them to be filled in. Check the
// gRPC function in question for more details.
message MatchObject{
string id = 1; // By convention, a UUID
message MatchObject {
string id = 1; // By convention, an Xid
string properties = 2; // By convention, a JSON-encoded string
string error = 3; // Last error encountered.
repeated Roster rosters = 4; // Rosters of players.
repeated PlayerPool pools = 5; // 'Hard' filters, and the players who match them.
string error = 3; // Last error encountered.
repeated Roster rosters = 4; // Rosters of players.
repeated PlayerPool pools = 5; // 'Hard' filters, and the players who match them.
string status = 6; // Resulting status of the match function
}
// Data structure to hold a list of players in a match.
message Roster{
string name = 1; // Arbitrary developer-chosen, human-readable string. By convention, set to team name.
// Data structure to hold a list of players in a match.
message Roster {
string name = 1; // Arbitrary developer-chosen, human-readable string. By convention, set to team name.
repeated Player players = 2; // Player profiles on this roster.
}
// A 'hard' filter to apply to the player pool.
message Filter{
string name = 1; // Arbitrary developer-chosen, human-readable name of this filter. Appears in logs and metrics.
message Filter {
string name = 1; // Arbitrary developer-chosen, human-readable name of this filter. Appears in logs and metrics.
string attribute = 2; // Name of the player attribute this filter operates on.
int64 maxv = 3; // Maximum value. Defaults to positive infinity (any value above minv).
int64 minv = 4; // Minimum value. Defaults to 0.
Stats stats = 5; // Statistics for the last time the filter was applied.
int64 minv = 4; // Minimum value. Defaults to 0.
Stats stats = 5; // Statistics for the last time the filter was applied.
}
// Holds statistics
message Stats{
message Stats {
int64 count = 1; // Number of results.
double elapsed = 2; // How long it took to get the results.
double elapsed = 2; // How long it took to get the results.
}
// PlayerPools are defined by a set of 'hard' filters, and can be filled in
@ -48,43 +63,40 @@ message Stats{
// PlayerPools contain a number of fields, but many gRPC calls that take a
// PlayerPool as input only require a few of them to be filled in. Check the
// gRPC function in question for more details.
message PlayerPool{
message PlayerPool {
string name = 1; // Arbitrary developer-chosen, human-readable string.
repeated Filter filters = 2; // Filters are logical AND-ed (a player must match every filter).
Roster roster = 3; // Roster of players that match all filters.
Stats stats = 4; // Statisticss for the last time this Pool was retrieved from state storage.
Stats stats = 4; // Statisticss for the last time this Pool was retrieved from state storage.
}
// Data structure to hold details about a player
message Player{
message Attribute{
string name = 1; // Name should match a Filter.attribute field.
// Open Match's internal representation and wire protocol format for "Players".
// In order to enter matchmaking using the Frontend API, your client code should generate
// a consistent (same result for each client every time they launch) with an ID and
// properties filled in (for more details about valid values for these fields,
// see the documentation).
// Players contain a number of fields, but the gRPC calls that take a
// Player as input only require a few of them to be filled in. Check the
// gRPC function in question for more details.
message Player {
message Attribute {
string name = 1; // Name should match a Filter.attribute field.
int64 value = 2;
}
string id = 1; // By convention, a UUID
string id = 1; // By convention, an Xid
string properties = 2; // By convention, a JSON-encoded string
string pool = 3; // Optionally used to specify the PlayerPool in which to find a player.
repeated Attribute attributes= 4; // Attributes of this player.
}
// Simple message to return success/failure and error status.
message Result{
bool success = 1;
string error = 2;
string pool = 3; // Optionally used to specify the PlayerPool in which to find a player.
repeated Attribute attributes = 4; // Attributes of this player.
string assignment = 5; // By convention, ip:port of a DGS to connect to
string status = 6; // Arbitrary developer-chosen string.
string error = 7; // Arbitrary developer-chosen string.
}
// IlInput is an empty message reserved for future use.
message IlInput{
message IlInput {
}
// Simple message used to pass the connection string for the DGS to the player.
// DEPRECATED: Likely to be integrated into another protobuf message in a future version.
message ConnectionInfo{
string connection_string = 1; // Passed by the matchmaker to game clients without modification.
}
message Assignments{
message Assignments {
repeated Roster rosters = 1;
ConnectionInfo connection_info = 2;
string assignment = 10;
}

@ -1,9 +1,63 @@
// Copyright 2018 Google LLC
//
// 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.
syntax = 'proto3';
package api;
option go_package = "github.com/GoogleCloudPlatform/open-match/internal/pb";
option go_package = "internal/pb";
// The protobuf messages sent in the gRPC calls are defined 'messages.proto'.
import 'api/protobuf-spec/messages.proto';
import 'google/api/annotations.proto';
message GetProfileRequest {
messages.MatchObject match = 1;
}
message GetProfileResponse {
messages.MatchObject match = 1;
}
message CreateProposalRequest {
messages.MatchObject match = 1;
}
message CreateProposalResponse {
}
message GetPlayerPoolRequest {
messages.PlayerPool player_pool = 1;
}
message GetPlayerPoolResponse {
messages.PlayerPool player_pool = 1;
}
message GetAllIgnoredPlayersRequest {
messages.IlInput ignore_player = 1;
}
message GetAllIgnoredPlayersResponse {
messages.Roster roster = 1;
}
message ListIgnoredPlayersRequest {
messages.IlInput ignore_player = 1;
}
message ListIgnoredPlayersResponse {
messages.Roster roster = 1;
}
// The MMLogic API provides utility functions for common MMF functionality, such
// as retreiving profiles and players from state storage, writing results to state storage,
@ -15,7 +69,9 @@ service MmLogic {
// 'filled' one.
// Note: filters are assumed to have been checked for validity by the
// backendapi when accepting a profile
rpc GetProfile(messages.MatchObject) returns (messages.MatchObject) {}
rpc GetProfile(GetProfileRequest) returns (GetProfileResponse) {
option (google.api.http).get = "/v1/logic/match-profiles/{match.id}";
}
// CreateProposal is called by MMFs that wish to write their results to
// a proposed MatchObject, that can be sent out the Backend API once it has
@ -50,22 +106,29 @@ service MmLogic {
// the backend api along with your match results.
// OUTPUT: a Result message with a boolean success value and an error string
// if an error was encountered
rpc CreateProposal(messages.MatchObject) returns (messages.Result) {}
rpc CreateProposal(CreateProposalRequest) returns (CreateProposalResponse) {
option (google.api.http) = {
put: "/v1/logic/match-proposals"
body: "*"
};
}
// Player listing and filtering functions
//
// RetrievePlayerPool gets the list of players that match every Filter in the
// PlayerPool, .excluding players in any configured ignore lists. It
// combines the results, and returns the resulting player pool.
rpc GetPlayerPool(messages.PlayerPool) returns (stream messages.PlayerPool) {}
rpc GetPlayerPool(GetPlayerPoolRequest) returns (stream GetPlayerPoolResponse) {
option (google.api.http).get = "/v1/logic/player-pools/{player_pool.name}";
}
// Ignore List functions
//
// IlInput is an empty message reserved for future use.
rpc GetAllIgnoredPlayers(messages.IlInput) returns (messages.Roster) {}
rpc GetAllIgnoredPlayers(GetAllIgnoredPlayersRequest) returns (GetAllIgnoredPlayersResponse) {}
// ListIgnoredPlayers retrieves players from the ignore list specified in the
// config file under 'ignoreLists.proposed.name'.
rpc ListIgnoredPlayers(messages.IlInput) returns (messages.Roster) {}
rpc ListIgnoredPlayers(ListIgnoredPlayersRequest) returns (ListIgnoredPlayersResponse) {}
// NYI
// UpdateMetrics sends stats about the MMF run to export to a metrics aggregation tool

@ -0,0 +1,380 @@
{
"swagger": "2.0",
"info": {
"title": "api/protobuf-spec/mmlogic.proto",
"version": "version not set"
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/logic/match-profiles/{match.id}": {
"get": {
"summary": "Send GetProfile a match object with the ID field populated, it will return a\n 'filled' one.\n Note: filters are assumed to have been checked for validity by the\n backendapi when accepting a profile",
"operationId": "GetProfile",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiGetProfileResponse"
}
}
},
"parameters": [
{
"name": "match.id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "match.properties",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "match.error",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "match.status",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"MmLogic"
]
}
},
"/v1/logic/match-proposals": {
"put": {
"summary": "CreateProposal is called by MMFs that wish to write their results to\na proposed MatchObject, that can be sent out the Backend API once it has\nbeen approved (by default, by the evaluator process).\n - adds all players in all Rosters to the proposed player ignore list\n - writes the proposed match to the provided key\n - adds that key to the list of proposals to be considered\nINPUT: \n * TO RETURN A MATCHOBJECT AFTER A SUCCESSFUL MMF RUN\n To create a match MatchObject message with these fields populated:\n - id, set to the value of the MMF_PROPOSAL_ID env var\n - properties\n - error. You must explicitly set this to an empty string if your MMF\n - roster, with the playerIDs filled in the 'players' repeated field. \n - [optional] pools, set to the output from the 'GetPlayerPools' call,\n will populate the pools with stats about how many players the filters\n matched and how long the filters took to run, which will be sent out\n the backend api along with your match results.\n was successful.\n * TO RETURN AN ERROR \n To report a failure or error, send a MatchObject message with these\n these fields populated:\n - id, set to the value of the MMF_ERROR_ID env var. \n - error, set to a string value describing the error your MMF encountered.\n - [optional] properties, anything you put here is returned to the\n backend along with your error.\n - [optional] rosters, anything you put here is returned to the\n backend along with your error.\n - [optional] pools, set to the output from the 'GetPlayerPools' call,\n will populate the pools with stats about how many players the filters\n matched and how long the filters took to run, which will be sent out\n the backend api along with your match results.\nOUTPUT: a Result message with a boolean success value and an error string\nif an error was encountered",
"operationId": "CreateProposal",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiCreateProposalResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiCreateProposalRequest"
}
}
],
"tags": [
"MmLogic"
]
}
},
"/v1/logic/player-pools/{player_pool.name}": {
"get": {
"summary": "Player listing and filtering functions",
"description": "RetrievePlayerPool gets the list of players that match every Filter in the\nPlayerPool, .excluding players in any configured ignore lists. It\ncombines the results, and returns the resulting player pool.",
"operationId": "GetPlayerPool",
"responses": {
"200": {
"description": "A successful response.(streaming responses)",
"schema": {
"$ref": "#/x-stream-definitions/apiGetPlayerPoolResponse"
}
}
},
"parameters": [
{
"name": "player_pool.name",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "player_pool.roster.name",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player_pool.stats.count",
"in": "query",
"required": false,
"type": "string",
"format": "int64"
},
{
"name": "player_pool.stats.elapsed",
"in": "query",
"required": false,
"type": "number",
"format": "double"
}
],
"tags": [
"MmLogic"
]
}
}
},
"definitions": {
"PlayerAttribute": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string",
"format": "int64"
}
}
},
"apiCreateProposalRequest": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
}
}
},
"apiCreateProposalResponse": {
"type": "object"
},
"apiGetAllIgnoredPlayersResponse": {
"type": "object",
"properties": {
"roster": {
"$ref": "#/definitions/messagesRoster"
}
}
},
"apiGetPlayerPoolResponse": {
"type": "object",
"properties": {
"player_pool": {
"$ref": "#/definitions/messagesPlayerPool"
}
}
},
"apiGetProfileResponse": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
}
}
},
"apiListIgnoredPlayersResponse": {
"type": "object",
"properties": {
"roster": {
"$ref": "#/definitions/messagesRoster"
}
}
},
"messagesFilter": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"attribute": {
"type": "string"
},
"maxv": {
"type": "string",
"format": "int64"
},
"minv": {
"type": "string",
"format": "int64"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "A 'hard' filter to apply to the player pool."
},
"messagesIlInput": {
"type": "object",
"description": "IlInput is an empty message reserved for future use."
},
"messagesMatchObject": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"error": {
"type": "string"
},
"rosters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesRoster"
}
},
"pools": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayerPool"
}
},
"status": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"MatchObjects\".\nIn order to request a match using the Backend API, your backend code should generate\na new MatchObject with an ID and properties filled in (for more details about valid\nvalues for these fields, see the documentation). Open Match then sends the Match\nObject through to your matchmaking function, where you add players to 'rosters' and\nstore any schemaless data you wish in the 'properties' field. The MatchObject\nis then sent, populated, out through the Backend API to your backend code.\n\nMatchObjects contain a number of fields, but many gRPC calls that take a\nMatchObject as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayer": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"pool": {
"type": "string"
},
"attributes": {
"type": "array",
"items": {
"$ref": "#/definitions/PlayerAttribute"
}
},
"assignment": {
"type": "string"
},
"status": {
"type": "string"
},
"error": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"Players\".\nIn order to enter matchmaking using the Frontend API, your client code should generate\na consistent (same result for each client every time they launch) with an ID and\nproperties filled in (for more details about valid values for these fields,\nsee the documentation).\nPlayers contain a number of fields, but the gRPC calls that take a\nPlayer as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayerPool": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesFilter"
}
},
"roster": {
"$ref": "#/definitions/messagesRoster"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "PlayerPools are defined by a set of 'hard' filters, and can be filled in\nwith the players that match those filters.\n\nPlayerPools contain a number of fields, but many gRPC calls that take a\nPlayerPool as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesRoster": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"players": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayer"
}
}
},
"description": "Data structure to hold a list of players in a match."
},
"messagesStats": {
"type": "object",
"properties": {
"count": {
"type": "string",
"format": "int64"
},
"elapsed": {
"type": "number",
"format": "double"
}
},
"title": "Holds statistics"
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string"
},
"value": {
"type": "string",
"format": "byte"
}
}
},
"runtimeStreamError": {
"type": "object",
"properties": {
"grpc_code": {
"type": "integer",
"format": "int32"
},
"http_code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"http_status": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
},
"x-stream-definitions": {
"apiGetPlayerPoolResponse": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/apiGetPlayerPoolResponse"
},
"error": {
"$ref": "#/definitions/runtimeStreamError"
}
},
"title": "Stream result of apiGetPlayerPoolResponse"
}
}
}

@ -1,3 +0,0 @@
python3 -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. mmlogic.proto
python3 -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. messages.proto
cp *pb2* $OM/examples/functions/python3/simple/.

@ -1,26 +0,0 @@
#!/bin/bash
# Script to compile golang versions of the OM proto files
#
# Copyright 2018 Google LLC
#
# 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.
cd $GOPATH/src
protoc \
${GOPATH}/src/github.com/GoogleCloudPlatform/open-match/api/protobuf-spec/backend.proto \
${GOPATH}/src/github.com/GoogleCloudPlatform/open-match/api/protobuf-spec/frontend.proto \
${GOPATH}/src/github.com/GoogleCloudPlatform/open-match/api/protobuf-spec/mmlogic.proto \
${GOPATH}/src/github.com/GoogleCloudPlatform/open-match/api/protobuf-spec/messages.proto \
-I ${GOPATH}/src/github.com/GoogleCloudPlatform/open-match/ \
--go_out=plugins=grpc:$GOPATH/src
cd -

207
cloudbuild.yaml Normal file

@ -0,0 +1,207 @@
# Copyright 2019 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.
################################################################################
# Open Match Script for Google Cloud Build #
################################################################################
# To run this locally:
# cloud-build-local --config=cloudbuild.yaml --dryrun=false --substitutions=_OM_VERSION=DEV .
# To run this remotely:
# gcloud builds submit --config=cloudbuild.yaml --substitutions=_OM_VERSION=DEV .
# Requires gcloud to be installed to work. (https://cloud.google.com/sdk/)
# gcloud auth login
# gcloud components install cloud-build-local
# This YAML contains all the build steps for building Open Match.
# All PRs are verified against this script to prevent build breakages and regressions.
# Conventions
# Each build step is ID'ed with "Prefix: Description".
# The prefix portion determines what kind of step it is and it's impact.
# Docker Image: Read-Only, outputs a docker image.
# Lint: Read-Only, verifies correctness and formatting of a file.
# Build: Read-Write, outputs a build artifact. Ok to run in parallel if the artifact will not collide with another one.
# Generate: Read-Write, outputs files within /workspace that are used in other build step. Do not run these in parallel.
# Setup: Read-Write, similar to generate but steps that run before any other step.
# Some useful things to know about Cloud Build.
# The root of this repository is always stored in /workspace.
# Any modifications that occur within /workspace are persisted between builds anything else is forgotten.
# If a build step has intermediate files that need to be persisted for a future step then use volumes.
# An example of this is the go-vol which is where the pkg/ data for go mod is stored.
# More information here: https://cloud.google.com/cloud-build/docs/build-config#build_steps
# A build step is basically a docker image that is tuned for Cloud Build,
# https://github.com/GoogleCloudPlatform/cloud-builders/tree/master/go
steps:
# Blocked by https://github.com/GoogleContainerTools/kaniko/issues/477
- id: 'Docker Image: open-match-build'
name: gcr.io/kaniko-project/executor
args: ['--destination=gcr.io/$PROJECT_ID/open-match-build', '--cache=true', '--cache-ttl=6h', '--dockerfile=Dockerfile.ci', '.']
waitFor: ['-']
- id: 'Build: Clean'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'clean']
waitFor: ['Docker Image: open-match-build']
- id: 'Setup: Download Dependencies'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'sync-deps']
volumes:
- name: 'go-vol'
path: '/go'
waitFor: ['Build: Clean']
- id: 'Build: Install Toolchain'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'install-toolchain']
volumes:
- name: 'go-vol'
path: '/go'
waitFor: ['Setup: Download Dependencies']
- id: 'Build: Protocol Buffers'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'all-protos']
volumes:
- name: 'go-vol'
path: '/go'
waitFor: ['Build: Install Toolchain']
- id: 'Build: Binaries'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'GOPROXY=off', 'all', '-j8']
volumes:
- name: 'go-vol'
path: '/go'
waitFor: ['Build: Protocol Buffers']
- id: 'Test: Core'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'GOPROXY=off', 'test-in-ci']
volumes:
- name: 'go-vol'
path: '/go'
waitFor: ['Build: Protocol Buffers']
- id: 'Build: Docker Images'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'VERSION_SUFFIX=$SHORT_SHA', 'build-images', '-j8']
waitFor: ['Build: Protocol Buffers']
- id: 'Build: Push Images'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'VERSION_SUFFIX=$SHORT_SHA', 'push-images', '-j8']
waitFor: ['Build: Docker Images']
- id: 'Build: Deployment Configs'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'VERSION_SUFFIX=$SHORT_SHA', 'clean-install-yaml', 'install/yaml/']
waitFor: ['Build: Install Toolchain']
- id: 'Lint: Format, Vet, Charts'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'lint']
volumes:
- name: 'go-vol'
path: '/go'
waitFor: ['Build: Protocol Buffers', 'Build: Deployment Configs']
- id: 'Build: Website'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'build/site/']
waitFor: ['Build: Install Toolchain']
- id: 'Test: Website'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'site-test']
waitFor: ['Build: Website']
- id: 'Deploy: Website'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', '_GCB_POST_SUBMIT=${_GCB_POST_SUBMIT}', VERSION_SUFFIX=$SHORT_SHA', 'BRANCH_NAME=$BRANCH_NAME', 'ci-deploy-dev-site']
waitFor: ['Test: Website', 'Build: Binaries']
volumes:
- name: 'go-vol'
path: '/go'
- id: 'Deploy: Deployment Configs'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', '_GCB_POST_SUBMIT=${_GCB_POST_SUBMIT}', VERSION_SUFFIX=$SHORT_SHA', 'BRANCH_NAME=$BRANCH_NAME', 'ci-deploy-artifacts']
waitFor: ['Lint: Format, Vet, Charts', 'Build: Binaries']
volumes:
- name: 'go-vol'
path: '/go'
#- id: 'Deploy: Create Cluster'
# name: 'gcr.io/$PROJECT_ID/open-match-build'
# args: ['make', 'create-gke-cluster', 'push-helm']
# waitFor: ['Build: Docker Images']
#- id: 'Deploy: Install Charts'
# name: 'gcr.io/$PROJECT_ID/open-match-build'
# args: ['make', 'sleep-10', 'install-chart', 'install-example-chart']
# waitFor: ['Deploy: Create Cluster']
#- id: 'Deploy: Teardown Cluster'
# name: 'gcr.io/$PROJECT_ID/open-match-build'
# args: ['make', 'sleep-10', 'delete-gke-cluster']
# waitFor: ['Deploy: Install Charts']
artifacts:
objects:
location: gs://open-match-build-artifacts/output/
paths:
- cmd/minimatch/minimatch
- cmd/backendapi/backendapi
- cmd/frontendapi/frontendapi
- cmd/mmlogicapi/mmlogicapi
- examples/functions/golang/grpc-serving/grpc-serving
- examples/evaluators/golang/serving/serving
- examples/backendclient/backendclient
- test/cmd/clientloadgen/clientloadgen
- test/cmd/frontendclient/frontendclient
- install/yaml/install.yaml
- install/yaml/install-example.yaml
- install/yaml/01-redis-chart.yaml
- install/yaml/02-open-match.yaml
- install/yaml/03-prometheus-chart.yaml
- install/yaml/04-grafana-chart.yaml
images:
- 'gcr.io/$PROJECT_ID/openmatch-minimatch:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-backendapi:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-frontendapi:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-mmlogicapi:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-evaluator-serving:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-mmf-go-grpc-serving-simple:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-backendclient:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-clientloadgen:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-frontendclient:${_OM_VERSION}-${SHORT_SHA}'
substitutions:
_OM_VERSION: "0.5.0-rc1"
_GCB_POST_SUBMIT: "0"
logsBucket: 'gs://open-match-build-logs/'
options:
sourceProvenanceHash: ['SHA256']
machineType: 'N1_HIGHCPU_8'
# TODO: The build is slow because we don't vendor. go get takes a very long time.
# Also we are rebuilding a lot of code unnecessarily. This should improve once
# we have new hermetic and reproducible Dockerfiles.
timeout: 1200s
# TODO Build Steps
# config/matchmaker_config.yaml: Lint this file so it's verified as a valid YAML file.
# examples/profiles/*.json: Verify valid JSON files.
#
# Consolidate many of these build steps via Makefile.
# Caching of dependencies is a serious problem. Cloud Build does not complete within 20 minutes!

@ -1,9 +0,0 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'--tag=gcr.io/$PROJECT_ID/openmatch-backendapi:dev',
'-f', 'Dockerfile.backendapi',
'.'
]
images: ['gcr.io/$PROJECT_ID/openmatch-backendapi:dev']

@ -1,10 +0,0 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'--tag=gcr.io/$PROJECT_ID/openmatch-devbase:latest',
'--cache-from=gcr.io/$PROJECT_ID/openmatch-devbase:latest',
'-f', 'Dockerfile.base',
'.'
]
images: ['gcr.io/$PROJECT_ID/openmatch-devbase:latest']

@ -1,10 +0,0 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'--tag=gcr.io/$PROJECT_ID/openmatch-evaluator:dev',
'-f', 'Dockerfile.evaluator',
'.'
]
images: ['gcr.io/$PROJECT_ID/openmatch-evaluator:dev']

@ -1,9 +0,0 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'--tag=gcr.io/$PROJECT_ID/openmatch-frontendapi:dev',
'-f', 'Dockerfile.frontendapi',
'.'
]
images: ['gcr.io/$PROJECT_ID/openmatch-frontendapi:dev']

@ -1,11 +0,0 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [ 'pull', 'gcr.io/$PROJECT_ID/openmatch-devbase:latest' ]
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'--tag=gcr.io/$PROJECT_ID/openmatch-mmf:go',
'-f', 'Dockerfile.mmf_go',
'.'
]
images: ['gcr.io/$PROJECT_ID/openmatch-mmf:go']

@ -1,9 +0,0 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'--tag=gcr.io/$PROJECT_ID/openmatch-mmf:php',
'-f', 'Dockerfile.mmf_php',
'.'
]
images: ['gcr.io/$PROJECT_ID/openmatch-mmf:php']

@ -1,12 +0,0 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [ 'pull', 'gcr.io/$PROJECT_ID/openmatch-mmf:py3' ]
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'--tag=gcr.io/$PROJECT_ID/openmatch-mmf:py3',
'--cache-from=gcr.io/$PROJECT_ID/openmatch-mmf:py3',
'-f', 'Dockerfile.mmf_py3',
'.'
]
images: ['gcr.io/$PROJECT_ID/openmatch-mmf:py3']

@ -1,12 +0,0 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [ 'pull', 'gcr.io/$PROJECT_ID/openmatch-mmforc:dev']
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'--tag=gcr.io/$PROJECT_ID/openmatch-mmforc:dev',
'--cache-from=gcr.io/$PROJECT_ID/openmatch-mmforc:dev',
'-f', 'Dockerfile.mmforc',
'.'
]
images: ['gcr.io/$PROJECT_ID/openmatch-mmforc:dev']

@ -1,12 +0,0 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [ 'pull', 'gcr.io/$PROJECT_ID/openmatch-mmlogicapi:dev' ]
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'--tag=gcr.io/$PROJECT_ID/openmatch-mmlogicapi:dev',
'--cache-from=gcr.io/$PROJECT_ID/openmatch-mmlogicapi:dev',
'-f', 'Dockerfile.mmlogicapi',
'.'
]
images: ['gcr.io/$PROJECT_ID/openmatch-mmlogicapi:dev']

23
cmd/backendapi/Dockerfile Normal file

@ -0,0 +1,23 @@
# Copyright 2019 Google LLC
#
# 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.
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/backendapi/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/backendapi/backendapi .
ENTRYPOINT ["/backendapi"]

@ -1,461 +0,0 @@
/*
package apisrv provides an implementation of the gRPC server defined in ../../../api/protobuf-spec/backend.proto
Copyright 2018 Google LLC
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
https://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 apisrv
import (
"context"
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/GoogleCloudPlatform/open-match/internal/metrics"
backend "github.com/GoogleCloudPlatform/open-match/internal/pb"
redisHelpers "github.com/GoogleCloudPlatform/open-match/internal/statestorage/redis"
"github.com/GoogleCloudPlatform/open-match/internal/statestorage/redis/ignorelist"
"github.com/GoogleCloudPlatform/open-match/internal/statestorage/redis/redispb"
"github.com/gogo/protobuf/jsonpb"
"github.com/gogo/protobuf/proto"
log "github.com/sirupsen/logrus"
"go.opencensus.io/plugin/ocgrpc"
"go.opencensus.io/stats"
"go.opencensus.io/tag"
"github.com/tidwall/gjson"
"github.com/gomodule/redigo/redis"
"github.com/google/uuid"
"github.com/spf13/viper"
"google.golang.org/grpc"
)
// Logrus structured logging setup
var (
beLogFields = log.Fields{
"app": "openmatch",
"component": "backend",
"caller": "backend/apisrv/apisrv.go",
}
beLog = log.WithFields(beLogFields)
)
// BackendAPI implements backend API Server, the server generated by compiling
// the protobuf, by fulfilling the API Client interface.
type BackendAPI struct {
grpc *grpc.Server
cfg *viper.Viper
pool *redis.Pool
}
type backendAPI BackendAPI
// New returns an instantiated srvice
func New(cfg *viper.Viper, pool *redis.Pool) *BackendAPI {
s := BackendAPI{
pool: pool,
grpc: grpc.NewServer(grpc.StatsHandler(&ocgrpc.ServerHandler{})),
cfg: cfg,
}
// Add a hook to the logger to auto-count log lines for metrics output thru OpenCensus
log.AddHook(metrics.NewHook(BeLogLines, KeySeverity))
backend.RegisterBackendServer(s.grpc, (*backendAPI)(&s))
beLog.Info("Successfully registered gRPC server")
return &s
}
// Open starts the api grpc service listening on the configured port.
func (s *BackendAPI) Open() error {
ln, err := net.Listen("tcp", ":"+s.cfg.GetString("api.backend.port"))
if err != nil {
beLog.WithFields(log.Fields{
"error": err.Error(),
"port": s.cfg.GetInt("api.backend.port"),
}).Error("net.Listen() error")
return err
}
beLog.WithFields(log.Fields{"port": s.cfg.GetInt("api.backend.port")}).Info("TCP net listener initialized")
go func() {
err := s.grpc.Serve(ln)
if err != nil {
beLog.WithFields(log.Fields{"error": err.Error()}).Error("gRPC serve() error")
}
beLog.Info("serving gRPC endpoints")
}()
return nil
}
// CreateMatch is this service's implementation of the CreateMatch gRPC method
// defined in ../proto/backend.proto
func (s *backendAPI) CreateMatch(c context.Context, profile *backend.MatchObject) (*backend.MatchObject, error) {
// Get a cancel-able context
ctx, cancel := context.WithCancel(c)
defer cancel()
// Create context for tagging OpenCensus metrics.
funcName := "CreateMatch"
fnCtx, _ := tag.New(ctx, tag.Insert(KeyMethod, funcName))
// Generate a request to fill the profile. Make a unique request ID.
moID := strings.Replace(uuid.New().String(), "-", "", -1)
requestKey := moID + "." + profile.Id
/*
// Debugging logs
beLog.Info("Pools nil? ", (profile.Pools == nil))
beLog.Info("Pools empty? ", (len(profile.Pools) == 0))
beLog.Info("Rosters nil? ", (profile.Rosters == nil))
beLog.Info("Rosters empty? ", (len(profile.Rosters) == 0))
beLog.Info("config set for json.pools?", s.cfg.IsSet("jsonkeys.pools"))
beLog.Info("contents key?", s.cfg.GetString("jsonkeys.pools"))
beLog.Info("contents exist?", gjson.Get(profile.Properties, s.cfg.GetString("jsonkeys.pools")).Exists())
*/
// Case where no protobuf pools was passed; check if there's a JSON version in the properties.
// This is for backwards compatibility, it is recommended you populate the
// pools before calling CreateMatch/ListMatches
if profile.Pools == nil && s.cfg.IsSet("jsonkeys.pools") &&
gjson.Get(profile.Properties, s.cfg.GetString("jsonkeys.pools")).Exists() {
poolsJSON := fmt.Sprintf("{\"pools\": %v}", gjson.Get(profile.Properties, s.cfg.GetString("jsonkeys.pools")).String())
ppLog := beLog.WithFields(log.Fields{"jsonkey": s.cfg.GetString("jsonkeys.pools")})
ppLog.Info("poolsJSON: ", poolsJSON)
ppools := &backend.MatchObject{}
err := jsonpb.UnmarshalString(poolsJSON, ppools)
if err != nil {
ppLog.Error("failed to parse JSON to protobuf pools")
} else {
profile.Pools = ppools.Pools
ppLog.Info("parsed JSON to protobuf pools")
}
}
// Case where no protobuf roster was passed; check if there's a JSON version in the properties.
// This is for backwards compatibility, it is recommended you populate the
// pools before calling CreateMatch/ListMatches
if profile.Rosters == nil && s.cfg.IsSet("jsonkeys.rosters") &&
gjson.Get(profile.Properties, s.cfg.GetString("jsonkeys.rosters")).Exists() {
rostersJSON := fmt.Sprintf("{\"rosters\": %v}", gjson.Get(profile.Properties, s.cfg.GetString("jsonkeys.rosters")).String())
rLog := beLog.WithFields(log.Fields{"jsonkey": s.cfg.GetString("jsonkeys.rosters")})
prosters := &backend.MatchObject{}
err := jsonpb.UnmarshalString(rostersJSON, prosters)
if err != nil {
rLog.Error("failed to parse JSON to protobuf rosters")
} else {
profile.Rosters = prosters.Rosters
rLog.Info("parsed JSON to protobuf rosters")
}
}
// Add fields for all subsequent logging
beLog = beLog.WithFields(log.Fields{
"profileID": profile.Id,
"func": funcName,
"matchObjectID": moID,
"requestKey": requestKey,
})
beLog.Info("gRPC call executing")
beLog.Info("profile is")
beLog.Info(profile)
// Write profile to state storage
//_, err := redisHelpers.Create(ctx, s.pool, profile.Id, profile.Properties)
err := redispb.MarshalToRedis(ctx, profile, s.pool)
if err != nil {
beLog.WithFields(log.Fields{
"error": err.Error(),
"component": "statestorage",
}).Error("State storage failure to create match profile")
// Failure! Return empty match object and the error
stats.Record(fnCtx, BeGrpcErrors.M(1))
return &backend.MatchObject{}, err
}
beLog.Info("Profile written to state storage")
// Queue the request ID to be sent to an MMF
_, err = redisHelpers.Update(ctx, s.pool, s.cfg.GetString("queues.profiles.name"), requestKey)
if err != nil {
beLog.WithFields(log.Fields{
"error": err.Error(),
"component": "statestorage",
}).Error("State storage failure to queue profile")
// Failure! Return empty match object and the error
stats.Record(fnCtx, BeGrpcErrors.M(1))
return &backend.MatchObject{}, err
}
beLog.Info("Profile added to processing queue")
// get and return matchobject, it will be written to the requestKey when the MMF has finished.
var ok bool
newMO := backend.MatchObject{Id: requestKey}
watchChan := redispb.Watcher(ctx, s.pool, newMO) // Watcher() runs the appropriate Redis commands.
errString := ("Error retrieving matchmaking results from state storage")
timeout := time.Duration(s.cfg.GetInt("interval.resultsTimeout")) * time.Second
select {
case <-time.After(timeout):
// TODO:Timeout: deal with the fallout. There are some edge cases here.
// When there is a timeout, need to send a stop to the watch channel.
stats.Record(fnCtx, BeGrpcRequests.M(1))
return profile, errors.New(errString + ": timeout exceeded")
case newMO, ok = <-watchChan:
if !ok {
// ok is false if watchChan has been closed by redispb.Watcher()
newMO.Error = newMO.Error + "; channel closed - was the context cancelled?"
} else {
// 'ok' was true, so properties should contain the results from redis.
// Do basic error checking on the returned JSON
if !gjson.Valid(profile.Properties) {
newMO.Error = "retreived properties json was malformed"
}
}
// TODO test that this is the correct condition for an empty error.
if newMO.Error != "" {
stats.Record(fnCtx, BeGrpcErrors.M(1))
return &newMO, errors.New(newMO.Error)
}
// Got results; close the channel so the Watcher() function stops querying redis.
}
beLog.Info("Matchmaking results received, returning to backend client")
stats.Record(fnCtx, BeGrpcRequests.M(1))
return &newMO, err
}
// ListMatches is this service's implementation of the ListMatches gRPC method
// defined in api/protobuf-spec/backend.proto
// This is the streaming version of CreateMatch - continually submitting the
// profile to be filled until the requesting service ends the connection.
func (s *backendAPI) ListMatches(p *backend.MatchObject, matchStream backend.Backend_ListMatchesServer) error {
// call creatematch in infinite loop as long as the stream is open
ctx := matchStream.Context() // https://talks.golang.org/2015/gotham-grpc.slide#30
// Create context for tagging OpenCensus metrics.
funcName := "ListMatches"
fnCtx, _ := tag.New(ctx, tag.Insert(KeyMethod, funcName))
beLog = beLog.WithFields(log.Fields{"func": funcName})
beLog.WithFields(log.Fields{
"profileID": p.Id,
}).Info("gRPC call executing. Calling CreateMatch. Looping until cancelled.")
for {
select {
case <-ctx.Done():
// Context cancelled, probably because the client cancelled their request, time to exit.
beLog.WithFields(log.Fields{
"profileID": p.Id,
}).Info("gRPC Context cancelled; client is probably finished receiving matches")
// TODO: need to make sure that in-flight matches don't get leaked here.
stats.Record(fnCtx, BeGrpcRequests.M(1))
return nil
default:
// Retreive results from Redis
requestProfile := proto.Clone(p).(*backend.MatchObject)
/*
beLog.Debug("new profile requested!")
beLog.Debug(requestProfile)
beLog.Debug(&requestProfile)
*/
mo, err := s.CreateMatch(ctx, requestProfile)
beLog = beLog.WithFields(log.Fields{"func": funcName})
if err != nil {
beLog.WithFields(log.Fields{"error": err.Error()}).Error("Failure calling CreateMatch")
stats.Record(fnCtx, BeGrpcErrors.M(1))
return err
}
beLog.WithFields(log.Fields{"matchProperties": fmt.Sprintf("%v", mo)}).Debug("Streaming back match object")
matchStream.Send(mo)
// TODO: This should be tunable, but there should be SOME sleep here, to give a requestor a window
// to cleanly close the connection after receiving a match object when they know they don't want to
// request any more matches.
time.Sleep(2 * time.Second)
}
}
}
// DeleteMatch is this service's implementation of the DeleteMatch gRPC method
// defined in ../proto/backend.proto
func (s *backendAPI) DeleteMatch(ctx context.Context, mo *backend.MatchObject) (*backend.Result, error) {
// Create context for tagging OpenCensus metrics.
funcName := "DeleteMatch"
fnCtx, _ := tag.New(ctx, tag.Insert(KeyMethod, funcName))
beLog = beLog.WithFields(log.Fields{"func": funcName})
beLog.WithFields(log.Fields{
"matchObjectID": mo.Id,
}).Info("gRPC call executing")
_, err := redisHelpers.Delete(ctx, s.pool, mo.Id)
if err != nil {
beLog.WithFields(log.Fields{
"error": err.Error(),
"component": "statestorage",
}).Error("State storage error")
stats.Record(fnCtx, BeGrpcErrors.M(1))
return &backend.Result{Success: false, Error: err.Error()}, err
}
beLog.WithFields(log.Fields{
"matchObjectID": mo.Id,
}).Info("Match Object deleted.")
stats.Record(fnCtx, BeGrpcRequests.M(1))
return &backend.Result{Success: true, Error: ""}, err
}
// CreateAssignments is this service's implementation of the CreateAssignments gRPC method
// defined in ../proto/backend.proto
func (s *backendAPI) CreateAssignments(ctx context.Context, a *backend.Assignments) (*backend.Result, error) {
assignments := make([]string, 0)
for _, roster := range a.Rosters {
assignments = append(assignments, getPlayerIdsFromRoster(roster)...)
}
// Create context for tagging OpenCensus metrics.
funcName := "CreateAssignments"
fnCtx, _ := tag.New(ctx, tag.Insert(KeyMethod, funcName))
beLog = beLog.WithFields(log.Fields{"func": funcName})
beLog.WithFields(log.Fields{
"numAssignments": len(assignments),
}).Info("gRPC call executing")
// TODO: relocate this redis functionality to a module
redisConn := s.pool.Get()
defer redisConn.Close()
// Create player assignments in a transaction.
redisConn.Send("MULTI")
for _, playerID := range assignments {
beLog.WithFields(log.Fields{
"query": "HSET",
"playerID": playerID,
s.cfg.GetString("jsonkeys.connstring"): a.ConnectionInfo.ConnectionString,
}).Debug("state storage operation")
redisConn.Send("HSET", playerID, s.cfg.GetString("jsonkeys.connstring"), a.ConnectionInfo.ConnectionString)
}
// Remove these players from the proposed list.
ignorelist.SendRemove(redisConn, "proposed", assignments)
// Add these players from the deindexed list.
ignorelist.SendAdd(redisConn, "deindexed", assignments)
// Send the multi-command transaction to Redis.
_, err := redisConn.Do("EXEC")
// Issue encountered
if err != nil {
beLog.WithFields(log.Fields{
"error": err.Error(),
"component": "statestorage",
}).Error("State storage error")
stats.Record(fnCtx, BeGrpcErrors.M(1))
stats.Record(fnCtx, BeAssignmentFailures.M(int64(len(assignments))))
return &backend.Result{Success: false, Error: err.Error()}, err
}
// Success!
beLog.WithFields(log.Fields{
"numAssignments": len(assignments),
}).Info("Assignments complete")
stats.Record(fnCtx, BeGrpcRequests.M(1))
stats.Record(fnCtx, BeAssignments.M(int64(len(assignments))))
return &backend.Result{Success: true, Error: ""}, err
}
// DeleteAssignments is this service's implementation of the DeleteAssignments gRPC method
// defined in ../proto/backend.proto
func (s *backendAPI) DeleteAssignments(ctx context.Context, r *backend.Roster) (*backend.Result, error) {
// TODO: make playerIDs a repeated protobuf message field and iterate over it
//assignments := strings.Split(a.PlayerIds, " ")
assignments := getPlayerIdsFromRoster(r)
// Create context for tagging OpenCensus metrics.
funcName := "DeleteAssignments"
fnCtx, _ := tag.New(ctx, tag.Insert(KeyMethod, funcName))
beLog = beLog.WithFields(log.Fields{"func": funcName})
beLog.WithFields(log.Fields{
"numAssignments": len(assignments),
}).Info("gRPC call executing")
// TODO: relocate this redis functionality to a module
redisConn := s.pool.Get()
defer redisConn.Close()
// Remove player assignments in a transaction
redisConn.Send("MULTI")
// TODO: make playerIDs a repeated protobuf message field and iterate over it
for _, playerID := range assignments {
beLog.WithFields(log.Fields{"query": "DEL", "key": playerID}).Debug("state storage operation")
redisConn.Send("DEL", playerID)
}
_, err := redisConn.Do("EXEC")
// Issue encountered
if err != nil {
beLog.WithFields(log.Fields{
"error": err.Error(),
"component": "statestorage",
}).Error("State storage error")
stats.Record(fnCtx, BeGrpcErrors.M(1))
stats.Record(fnCtx, BeAssignmentDeletionFailures.M(int64(len(assignments))))
return &backend.Result{Success: false, Error: err.Error()}, err
}
// Success!
stats.Record(fnCtx, BeGrpcRequests.M(1))
stats.Record(fnCtx, BeAssignmentDeletions.M(int64(len(assignments))))
return &backend.Result{Success: true, Error: ""}, err
}
func getPlayerIdsFromRoster(r *backend.Roster) []string {
playerIDs := make([]string, 0)
for _, p := range r.Players {
playerIDs = append(playerIDs, p.Id)
}
return playerIDs
}

@ -1,6 +1,7 @@
/*
This application handles all the startup and connection scaffolding for
running a gRPC server serving the APIService as defined in proto/backend.proto
running a gRPC server serving the APIService as defined in
${OM_ROOT}/internal/pb/backend.pb.go
All the actual important bits are in the API Server source code: apisrv/apisrv.go
@ -22,84 +23,9 @@ limitations under the License.
package main
import (
"errors"
"os"
"os/signal"
"github.com/GoogleCloudPlatform/open-match/cmd/backendapi/apisrv"
"github.com/GoogleCloudPlatform/open-match/config"
"github.com/GoogleCloudPlatform/open-match/internal/metrics"
redishelpers "github.com/GoogleCloudPlatform/open-match/internal/statestorage/redis"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"go.opencensus.io/plugin/ocgrpc"
"github.com/GoogleCloudPlatform/open-match/internal/app/backendapi"
)
var (
// Logrus structured logging setup
beLogFields = log.Fields{
"app": "openmatch",
"component": "backend",
"caller": "backendapi/main.go",
}
beLog = log.WithFields(beLogFields)
// Viper config management setup
cfg = viper.New()
err = errors.New("")
)
func init() {
// Logrus structured logging initialization
// Add a hook to the logger to auto-count log lines for metrics output thru OpenCensus
log.AddHook(metrics.NewHook(apisrv.BeLogLines, apisrv.KeySeverity))
// Viper config management initialization
cfg, err = config.Read()
if err != nil {
beLog.WithFields(log.Fields{
"error": err.Error(),
}).Error("Unable to load config file")
}
if cfg.GetBool("debug") == true {
log.SetLevel(log.DebugLevel) // debug only, verbose - turn off in production!
beLog.Warn("Debug logging configured. Not recommended for production!")
}
// Configure OpenCensus exporter to Prometheus
// metrics.ConfigureOpenCensusPrometheusExporter expects that every OpenCensus view you
// want to register is in an array, so append any views you want from other
// packages to a single array here.
ocServerViews := apisrv.DefaultBackendAPIViews // BackendAPI OpenCensus views.
ocServerViews = append(ocServerViews, ocgrpc.DefaultServerViews...) // gRPC OpenCensus views.
ocServerViews = append(ocServerViews, config.CfgVarCountView) // config loader view.
// Waiting on https://github.com/opencensus-integrations/redigo/pull/1
// ocServerViews = append(ocServerViews, redis.ObservabilityMetricViews...) // redis OpenCensus views.
beLog.WithFields(log.Fields{"viewscount": len(ocServerViews)}).Info("Loaded OpenCensus views")
metrics.ConfigureOpenCensusPrometheusExporter(cfg, ocServerViews)
}
func main() {
// Connect to redis
pool := redishelpers.ConnectionPool(cfg)
defer pool.Close()
// Instantiate the gRPC server with the connections we've made
beLog.WithFields(log.Fields{"testfield": "test"}).Info("Attempting to start gRPC server")
srv := apisrv.New(cfg, pool)
// Run the gRPC server
err := srv.Open()
if err != nil {
beLog.WithFields(log.Fields{"error": err.Error()}).Fatal("Failed to start gRPC server")
}
// Exit when we see a signal
terminate := make(chan os.Signal, 1)
signal.Notify(terminate, os.Interrupt)
<-terminate
beLog.Info("Shutting down gRPC server")
backendapi.RunApplication()
}

@ -1 +0,0 @@
../../config/matchmaker_config.json

@ -1,749 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: backend.proto
/*
Package backend is a generated protocol buffer package.
It is generated from these files:
backend.proto
It has these top-level messages:
Profile
MatchObject
Roster
Filter
Stats
PlayerPool
Player
Result
IlInput
Timestamp
ConnectionInfo
Assignments
*/
package backend
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type Profile struct {
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
Properties string `protobuf:"bytes,2,opt,name=properties" json:"properties,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name" json:"name,omitempty"`
// When you send a Profile to the backendAPI, it looks to see if you populated
// this field with protobuf-encoded PlayerPool objects containing valid the filters
// objects. If you did, they are used by OM. If you didn't, the backendAPI
// next looks in your properties blob at the key specified in the 'jsonkeys.pools'
// config value from config/matchmaker_config.json - If it finds valid player
// pool definitions at that key, it will try to unmarshal them into this field.
// If you didn't specify valid player pools in either place, OM assumes you
// know what you're doing and just leaves this unpopulatd.
Pools []*PlayerPool `protobuf:"bytes,4,rep,name=pools" json:"pools,omitempty"`
}
func (m *Profile) Reset() { *m = Profile{} }
func (m *Profile) String() string { return proto.CompactTextString(m) }
func (*Profile) ProtoMessage() {}
func (*Profile) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *Profile) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *Profile) GetProperties() string {
if m != nil {
return m.Properties
}
return ""
}
func (m *Profile) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Profile) GetPools() []*PlayerPool {
if m != nil {
return m.Pools
}
return nil
}
// A MMF takes the Profile object above, and generates a MatchObject.
type MatchObject struct {
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
Properties string `protobuf:"bytes,2,opt,name=properties" json:"properties,omitempty"`
Rosters []*Roster `protobuf:"bytes,3,rep,name=rosters" json:"rosters,omitempty"`
Pools []*PlayerPool `protobuf:"bytes,4,rep,name=pools" json:"pools,omitempty"`
}
func (m *MatchObject) Reset() { *m = MatchObject{} }
func (m *MatchObject) String() string { return proto.CompactTextString(m) }
func (*MatchObject) ProtoMessage() {}
func (*MatchObject) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *MatchObject) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *MatchObject) GetProperties() string {
if m != nil {
return m.Properties
}
return ""
}
func (m *MatchObject) GetRosters() []*Roster {
if m != nil {
return m.Rosters
}
return nil
}
func (m *MatchObject) GetPools() []*PlayerPool {
if m != nil {
return m.Pools
}
return nil
}
// Data structure to hold a list of players in a match.
type Roster struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Players []*Player `protobuf:"bytes,2,rep,name=players" json:"players,omitempty"`
}
func (m *Roster) Reset() { *m = Roster{} }
func (m *Roster) String() string { return proto.CompactTextString(m) }
func (*Roster) ProtoMessage() {}
func (*Roster) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
func (m *Roster) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Roster) GetPlayers() []*Player {
if m != nil {
return m.Players
}
return nil
}
// A filter to apply to the player pool.
type Filter struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Attribute string `protobuf:"bytes,2,opt,name=attribute" json:"attribute,omitempty"`
Maxv int64 `protobuf:"varint,3,opt,name=maxv" json:"maxv,omitempty"`
Minv int64 `protobuf:"varint,4,opt,name=minv" json:"minv,omitempty"`
Stats *Stats `protobuf:"bytes,5,opt,name=stats" json:"stats,omitempty"`
}
func (m *Filter) Reset() { *m = Filter{} }
func (m *Filter) String() string { return proto.CompactTextString(m) }
func (*Filter) ProtoMessage() {}
func (*Filter) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
func (m *Filter) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Filter) GetAttribute() string {
if m != nil {
return m.Attribute
}
return ""
}
func (m *Filter) GetMaxv() int64 {
if m != nil {
return m.Maxv
}
return 0
}
func (m *Filter) GetMinv() int64 {
if m != nil {
return m.Minv
}
return 0
}
func (m *Filter) GetStats() *Stats {
if m != nil {
return m.Stats
}
return nil
}
type Stats struct {
Count int64 `protobuf:"varint,1,opt,name=count" json:"count,omitempty"`
Elapsed float64 `protobuf:"fixed64,2,opt,name=elapsed" json:"elapsed,omitempty"`
}
func (m *Stats) Reset() { *m = Stats{} }
func (m *Stats) String() string { return proto.CompactTextString(m) }
func (*Stats) ProtoMessage() {}
func (*Stats) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} }
func (m *Stats) GetCount() int64 {
if m != nil {
return m.Count
}
return 0
}
func (m *Stats) GetElapsed() float64 {
if m != nil {
return m.Elapsed
}
return 0
}
type PlayerPool struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Filters []*Filter `protobuf:"bytes,2,rep,name=filters" json:"filters,omitempty"`
Roster *Roster `protobuf:"bytes,3,opt,name=roster" json:"roster,omitempty"`
Stats *Stats `protobuf:"bytes,4,opt,name=stats" json:"stats,omitempty"`
}
func (m *PlayerPool) Reset() { *m = PlayerPool{} }
func (m *PlayerPool) String() string { return proto.CompactTextString(m) }
func (*PlayerPool) ProtoMessage() {}
func (*PlayerPool) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} }
func (m *PlayerPool) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *PlayerPool) GetFilters() []*Filter {
if m != nil {
return m.Filters
}
return nil
}
func (m *PlayerPool) GetRoster() *Roster {
if m != nil {
return m.Roster
}
return nil
}
func (m *PlayerPool) GetStats() *Stats {
if m != nil {
return m.Stats
}
return nil
}
// Data structure for a profile to pass to the matchmaking function.
type Player struct {
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
Properties string `protobuf:"bytes,2,opt,name=properties" json:"properties,omitempty"`
Pool string `protobuf:"bytes,3,opt,name=pool" json:"pool,omitempty"`
Attributes []*Player_Attribute `protobuf:"bytes,4,rep,name=attributes" json:"attributes,omitempty"`
}
func (m *Player) Reset() { *m = Player{} }
func (m *Player) String() string { return proto.CompactTextString(m) }
func (*Player) ProtoMessage() {}
func (*Player) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} }
func (m *Player) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *Player) GetProperties() string {
if m != nil {
return m.Properties
}
return ""
}
func (m *Player) GetPool() string {
if m != nil {
return m.Pool
}
return ""
}
func (m *Player) GetAttributes() []*Player_Attribute {
if m != nil {
return m.Attributes
}
return nil
}
type Player_Attribute struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Value int64 `protobuf:"varint,2,opt,name=value" json:"value,omitempty"`
}
func (m *Player_Attribute) Reset() { *m = Player_Attribute{} }
func (m *Player_Attribute) String() string { return proto.CompactTextString(m) }
func (*Player_Attribute) ProtoMessage() {}
func (*Player_Attribute) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6, 0} }
func (m *Player_Attribute) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Player_Attribute) GetValue() int64 {
if m != nil {
return m.Value
}
return 0
}
// Simple message to return success/failure and error status.
type Result struct {
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
Error string `protobuf:"bytes,2,opt,name=error" json:"error,omitempty"`
}
func (m *Result) Reset() { *m = Result{} }
func (m *Result) String() string { return proto.CompactTextString(m) }
func (*Result) ProtoMessage() {}
func (*Result) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} }
func (m *Result) GetSuccess() bool {
if m != nil {
return m.Success
}
return false
}
func (m *Result) GetError() string {
if m != nil {
return m.Error
}
return ""
}
// IlInput is an empty message reserved for future use.
type IlInput struct {
}
func (m *IlInput) Reset() { *m = IlInput{} }
func (m *IlInput) String() string { return proto.CompactTextString(m) }
func (*IlInput) ProtoMessage() {}
func (*IlInput) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} }
// Epoch timestamp in seconds.
type Timestamp struct {
Ts int64 `protobuf:"varint,1,opt,name=ts" json:"ts,omitempty"`
}
func (m *Timestamp) Reset() { *m = Timestamp{} }
func (m *Timestamp) String() string { return proto.CompactTextString(m) }
func (*Timestamp) ProtoMessage() {}
func (*Timestamp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} }
func (m *Timestamp) GetTs() int64 {
if m != nil {
return m.Ts
}
return 0
}
// Simple message used to pass the connection string for the DGS to the player.
type ConnectionInfo struct {
ConnectionString string `protobuf:"bytes,1,opt,name=connection_string,json=connectionString" json:"connection_string,omitempty"`
}
func (m *ConnectionInfo) Reset() { *m = ConnectionInfo{} }
func (m *ConnectionInfo) String() string { return proto.CompactTextString(m) }
func (*ConnectionInfo) ProtoMessage() {}
func (*ConnectionInfo) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} }
func (m *ConnectionInfo) GetConnectionString() string {
if m != nil {
return m.ConnectionString
}
return ""
}
type Assignments struct {
Rosters []*Roster `protobuf:"bytes,1,rep,name=rosters" json:"rosters,omitempty"`
ConnectionInfo *ConnectionInfo `protobuf:"bytes,2,opt,name=connection_info,json=connectionInfo" json:"connection_info,omitempty"`
}
func (m *Assignments) Reset() { *m = Assignments{} }
func (m *Assignments) String() string { return proto.CompactTextString(m) }
func (*Assignments) ProtoMessage() {}
func (*Assignments) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11} }
func (m *Assignments) GetRosters() []*Roster {
if m != nil {
return m.Rosters
}
return nil
}
func (m *Assignments) GetConnectionInfo() *ConnectionInfo {
if m != nil {
return m.ConnectionInfo
}
return nil
}
func init() {
proto.RegisterType((*Profile)(nil), "Profile")
proto.RegisterType((*MatchObject)(nil), "MatchObject")
proto.RegisterType((*Roster)(nil), "Roster")
proto.RegisterType((*Filter)(nil), "Filter")
proto.RegisterType((*Stats)(nil), "Stats")
proto.RegisterType((*PlayerPool)(nil), "PlayerPool")
proto.RegisterType((*Player)(nil), "Player")
proto.RegisterType((*Player_Attribute)(nil), "Player.Attribute")
proto.RegisterType((*Result)(nil), "Result")
proto.RegisterType((*IlInput)(nil), "IlInput")
proto.RegisterType((*Timestamp)(nil), "Timestamp")
proto.RegisterType((*ConnectionInfo)(nil), "ConnectionInfo")
proto.RegisterType((*Assignments)(nil), "Assignments")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for API service
type APIClient interface {
// Calls to ask the matchmaker to run a matchmaking function.
//
// Run MMF once. Return a matchobject that fits this profile.
CreateMatch(ctx context.Context, in *Profile, opts ...grpc.CallOption) (*MatchObject, error)
// Continually run MMF and stream matchobjects that fit this profile until
// client closes the connection.
ListMatches(ctx context.Context, in *Profile, opts ...grpc.CallOption) (API_ListMatchesClient, error)
// Delete a matchobject from state storage manually. (Matchobjects in state
// storage will also automatically expire after a while)
DeleteMatch(ctx context.Context, in *MatchObject, opts ...grpc.CallOption) (*Result, error)
// Call for communication of connection info to players.
//
// Write the connection info for the list of players in the
// Assignments.Rosters to state storage. The FrontendAPI is responsible for
// sending anything written here to the game clients.
// TODO: change this to be agnostic; return a 'result' instead of a connection
// string so it can be integrated with session service etc
CreateAssignments(ctx context.Context, in *Assignments, opts ...grpc.CallOption) (*Result, error)
// Remove DGS connection info from state storage for all players in the Roster.
DeleteAssignments(ctx context.Context, in *Roster, opts ...grpc.CallOption) (*Result, error)
}
type aPIClient struct {
cc *grpc.ClientConn
}
func NewAPIClient(cc *grpc.ClientConn) APIClient {
return &aPIClient{cc}
}
func (c *aPIClient) CreateMatch(ctx context.Context, in *Profile, opts ...grpc.CallOption) (*MatchObject, error) {
out := new(MatchObject)
err := grpc.Invoke(ctx, "/API/CreateMatch", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aPIClient) ListMatches(ctx context.Context, in *Profile, opts ...grpc.CallOption) (API_ListMatchesClient, error) {
stream, err := grpc.NewClientStream(ctx, &_API_serviceDesc.Streams[0], c.cc, "/API/ListMatches", opts...)
if err != nil {
return nil, err
}
x := &aPIListMatchesClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type API_ListMatchesClient interface {
Recv() (*MatchObject, error)
grpc.ClientStream
}
type aPIListMatchesClient struct {
grpc.ClientStream
}
func (x *aPIListMatchesClient) Recv() (*MatchObject, error) {
m := new(MatchObject)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *aPIClient) DeleteMatch(ctx context.Context, in *MatchObject, opts ...grpc.CallOption) (*Result, error) {
out := new(Result)
err := grpc.Invoke(ctx, "/API/DeleteMatch", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aPIClient) CreateAssignments(ctx context.Context, in *Assignments, opts ...grpc.CallOption) (*Result, error) {
out := new(Result)
err := grpc.Invoke(ctx, "/API/CreateAssignments", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aPIClient) DeleteAssignments(ctx context.Context, in *Roster, opts ...grpc.CallOption) (*Result, error) {
out := new(Result)
err := grpc.Invoke(ctx, "/API/DeleteAssignments", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for API service
type APIServer interface {
// Calls to ask the matchmaker to run a matchmaking function.
//
// Run MMF once. Return a matchobject that fits this profile.
CreateMatch(context.Context, *Profile) (*MatchObject, error)
// Continually run MMF and stream matchobjects that fit this profile until
// client closes the connection.
ListMatches(*Profile, API_ListMatchesServer) error
// Delete a matchobject from state storage manually. (Matchobjects in state
// storage will also automatically expire after a while)
DeleteMatch(context.Context, *MatchObject) (*Result, error)
// Call for communication of connection info to players.
//
// Write the connection info for the list of players in the
// Assignments.Rosters to state storage. The FrontendAPI is responsible for
// sending anything written here to the game clients.
// TODO: change this to be agnostic; return a 'result' instead of a connection
// string so it can be integrated with session service etc
CreateAssignments(context.Context, *Assignments) (*Result, error)
// Remove DGS connection info from state storage for all players in the Roster.
DeleteAssignments(context.Context, *Roster) (*Result, error)
}
func RegisterAPIServer(s *grpc.Server, srv APIServer) {
s.RegisterService(&_API_serviceDesc, srv)
}
func _API_CreateMatch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Profile)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(APIServer).CreateMatch(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/API/CreateMatch",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(APIServer).CreateMatch(ctx, req.(*Profile))
}
return interceptor(ctx, in, info, handler)
}
func _API_ListMatches_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(Profile)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(APIServer).ListMatches(m, &aPIListMatchesServer{stream})
}
type API_ListMatchesServer interface {
Send(*MatchObject) error
grpc.ServerStream
}
type aPIListMatchesServer struct {
grpc.ServerStream
}
func (x *aPIListMatchesServer) Send(m *MatchObject) error {
return x.ServerStream.SendMsg(m)
}
func _API_DeleteMatch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MatchObject)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(APIServer).DeleteMatch(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/API/DeleteMatch",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(APIServer).DeleteMatch(ctx, req.(*MatchObject))
}
return interceptor(ctx, in, info, handler)
}
func _API_CreateAssignments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Assignments)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(APIServer).CreateAssignments(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/API/CreateAssignments",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(APIServer).CreateAssignments(ctx, req.(*Assignments))
}
return interceptor(ctx, in, info, handler)
}
func _API_DeleteAssignments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Roster)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(APIServer).DeleteAssignments(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/API/DeleteAssignments",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(APIServer).DeleteAssignments(ctx, req.(*Roster))
}
return interceptor(ctx, in, info, handler)
}
var _API_serviceDesc = grpc.ServiceDesc{
ServiceName: "API",
HandlerType: (*APIServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CreateMatch",
Handler: _API_CreateMatch_Handler,
},
{
MethodName: "DeleteMatch",
Handler: _API_DeleteMatch_Handler,
},
{
MethodName: "CreateAssignments",
Handler: _API_CreateAssignments_Handler,
},
{
MethodName: "DeleteAssignments",
Handler: _API_DeleteAssignments_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "ListMatches",
Handler: _API_ListMatches_Handler,
ServerStreams: true,
},
},
Metadata: "backend.proto",
}
func init() { proto.RegisterFile("backend.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 591 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x54, 0x51, 0x6f, 0xd3, 0x30,
0x10, 0x9e, 0x9b, 0x26, 0x59, 0x2f, 0x63, 0xa3, 0xd6, 0x1e, 0xa2, 0x31, 0x41, 0xe7, 0x07, 0x56,
0x04, 0x8a, 0xa0, 0x08, 0xb1, 0x17, 0x84, 0xaa, 0x21, 0xa4, 0x4a, 0x20, 0x2a, 0x8f, 0x77, 0x94,
0xa6, 0xee, 0xf0, 0x48, 0xed, 0xc8, 0x76, 0x2a, 0x78, 0x43, 0xf0, 0x9f, 0xf8, 0x2d, 0xfc, 0x1c,
0x14, 0x3b, 0x69, 0x53, 0x41, 0x25, 0xe0, 0xcd, 0xdf, 0xe7, 0xbb, 0xf3, 0x77, 0xdf, 0xe5, 0x02,
0xb7, 0x66, 0x69, 0xf6, 0x89, 0x89, 0x79, 0x52, 0x28, 0x69, 0x24, 0x29, 0x20, 0x9c, 0x2a, 0xb9,
0xe0, 0x39, 0xc3, 0x87, 0xd0, 0xe1, 0xf3, 0x18, 0x0d, 0xd0, 0xb0, 0x47, 0x3b, 0x7c, 0x8e, 0xef,
0x02, 0x14, 0x4a, 0x16, 0x4c, 0x19, 0xce, 0x74, 0xdc, 0xb1, 0x7c, 0x8b, 0xc1, 0x18, 0xba, 0x22,
0x5d, 0xb2, 0xd8, 0xb3, 0x37, 0xf6, 0x8c, 0xcf, 0xc0, 0x2f, 0xa4, 0xcc, 0x75, 0xdc, 0x1d, 0x78,
0xc3, 0x68, 0x14, 0x25, 0xd3, 0x3c, 0xfd, 0xc2, 0xd4, 0x54, 0xca, 0x9c, 0xba, 0x1b, 0xf2, 0x1d,
0x41, 0xf4, 0x36, 0x35, 0xd9, 0xc7, 0x77, 0xb3, 0x1b, 0x96, 0x99, 0x7f, 0x7e, 0xf6, 0x0c, 0x42,
0x25, 0xb5, 0x61, 0x4a, 0xc7, 0x9e, 0x7d, 0x24, 0x4c, 0xa8, 0xc5, 0xb4, 0xe1, 0xff, 0x46, 0xc5,
0x4b, 0x08, 0x5c, 0xd6, 0xba, 0x0d, 0xb4, 0xd5, 0x46, 0x58, 0xd8, 0x94, 0x4a, 0x80, 0x7b, 0xc3,
0x95, 0xa0, 0x0d, 0x4f, 0xbe, 0x22, 0x08, 0x5e, 0xf3, 0x7c, 0x57, 0x85, 0x53, 0xe8, 0xa5, 0xc6,
0x28, 0x3e, 0x2b, 0x0d, 0xab, 0x9b, 0xd8, 0x10, 0x55, 0xc6, 0x32, 0xfd, 0xbc, 0xb2, 0xd6, 0x79,
0xd4, 0x9e, 0x2d, 0xc7, 0xc5, 0x2a, 0xee, 0xd6, 0x1c, 0x17, 0x2b, 0x7c, 0x0a, 0xbe, 0x36, 0xa9,
0xd1, 0xb1, 0x3f, 0x40, 0xc3, 0x68, 0x14, 0x24, 0x57, 0x15, 0xa2, 0x8e, 0x24, 0xcf, 0xc1, 0xb7,
0x18, 0x1f, 0x83, 0x9f, 0xc9, 0x52, 0x18, 0xab, 0xc0, 0xa3, 0x0e, 0xe0, 0x18, 0x42, 0x96, 0xa7,
0x85, 0x66, 0x73, 0x2b, 0x00, 0xd1, 0x06, 0x92, 0x6f, 0x08, 0x60, 0x63, 0xc9, 0x2e, 0x07, 0x16,
0xb6, 0xbb, 0x8d, 0x03, 0xae, 0x5b, 0xda, 0xf0, 0xf8, 0x1e, 0x04, 0xce, 0x70, 0xdb, 0x46, 0x6b,
0x0e, 0x35, 0xbd, 0x51, 0xdf, 0xfd, 0x93, 0xfa, 0x1f, 0x08, 0x02, 0x27, 0xe2, 0x7f, 0xbe, 0xbc,
0x6a, 0x8a, 0xcd, 0x97, 0x57, 0x9d, 0xf1, 0x13, 0x80, 0xb5, 0xbf, 0xcd, 0xe0, 0xfb, 0xf5, 0xd4,
0x92, 0x71, 0x73, 0x43, 0x5b, 0x41, 0x27, 0xcf, 0xa0, 0x37, 0x6e, 0x8f, 0xe4, 0x37, 0x13, 0x8e,
0xc1, 0x5f, 0xa5, 0x79, 0xe9, 0x06, 0xe8, 0x51, 0x07, 0xc8, 0x05, 0x04, 0x94, 0xe9, 0x32, 0xb7,
0x0e, 0xeb, 0x32, 0xcb, 0x98, 0xd6, 0x36, 0x6d, 0x9f, 0x36, 0xb0, 0xca, 0x64, 0x4a, 0x49, 0x55,
0x8b, 0x77, 0x80, 0xf4, 0x20, 0x9c, 0xe4, 0x13, 0x51, 0x94, 0x86, 0xdc, 0x81, 0xde, 0x7b, 0xbe,
0x64, 0xda, 0xa4, 0xcb, 0xa2, 0xea, 0xdf, 0xe8, 0x7a, 0x78, 0x1d, 0xa3, 0xc9, 0x0b, 0x38, 0xbc,
0x94, 0x42, 0xb0, 0xcc, 0x70, 0x29, 0x26, 0x62, 0x21, 0xf1, 0x43, 0xe8, 0x67, 0x6b, 0xe6, 0x83,
0x36, 0x8a, 0x8b, 0xeb, 0x5a, 0xea, 0xed, 0xcd, 0xc5, 0x95, 0xe5, 0xc9, 0x0d, 0x44, 0x63, 0xad,
0xf9, 0xb5, 0x58, 0x32, 0x61, 0xb6, 0x16, 0x06, 0xed, 0x58, 0x98, 0x0b, 0x38, 0x6a, 0x95, 0xe7,
0x62, 0x21, 0xad, 0xf0, 0x68, 0x74, 0x94, 0x6c, 0x0b, 0xa1, 0x87, 0xd9, 0x16, 0x1e, 0xfd, 0x44,
0xe0, 0x8d, 0xa7, 0x13, 0x7c, 0x0e, 0xd1, 0xa5, 0x62, 0xa9, 0x61, 0x76, 0xb5, 0xf1, 0x7e, 0x52,
0xff, 0x55, 0x4e, 0x0e, 0x92, 0xd6, 0xb2, 0x93, 0x3d, 0xfc, 0x00, 0xa2, 0x37, 0x5c, 0x1b, 0x4b,
0x32, 0xbd, 0x3b, 0xf0, 0x31, 0xc2, 0xf7, 0x21, 0x7a, 0xc5, 0x72, 0xd6, 0xd4, 0xdc, 0x0a, 0x38,
0x09, 0x13, 0x37, 0x04, 0xb2, 0x87, 0x1f, 0x41, 0xdf, 0xbd, 0xdd, 0xee, 0xfa, 0x20, 0x69, 0xa1,
0x76, 0xf4, 0x39, 0xf4, 0x5d, 0xd5, 0x76, 0x74, 0x63, 0x49, 0x2b, 0x70, 0x16, 0xd8, 0x3f, 0xe4,
0xd3, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x04, 0x1a, 0xf8, 0x0a, 0x32, 0x05, 0x00, 0x00,
}

@ -1,4 +0,0 @@
/*
backend is a package compiled from the protobuffer in <REPO_ROOT>/api/protobuf-spec/backend.proto. It is auto-generated and shouldn't be edited.
*/
package backend

@ -0,0 +1,23 @@
# Copyright 2019 Google LLC
#
# 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.
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/frontendapi/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/frontendapi/frontendapi .
ENTRYPOINT ["/frontendapi"]

@ -1,300 +0,0 @@
/*
package apisrv provides an implementation of the gRPC server defined in ../../../api/protobuf-spec/frontend.proto.
Copyright 2018 Google LLC
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
https://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 apisrv
import (
"context"
"errors"
"net"
"time"
frontend "github.com/GoogleCloudPlatform/open-match/cmd/frontendapi/proto"
"github.com/GoogleCloudPlatform/open-match/internal/metrics"
playerq "github.com/GoogleCloudPlatform/open-match/internal/statestorage/redis/playerq"
log "github.com/sirupsen/logrus"
"go.opencensus.io/stats"
"go.opencensus.io/tag"
"github.com/gomodule/redigo/redis"
"github.com/spf13/viper"
"go.opencensus.io/plugin/ocgrpc"
"google.golang.org/grpc"
)
// Logrus structured logging setup
var (
feLogFields = log.Fields{
"app": "openmatch",
"component": "frontend",
"caller": "frontendapi/apisrv/apisrv.go",
}
feLog = log.WithFields(feLogFields)
)
// FrontendAPI implements frontend.ApiServer, the server generated by compiling
// the protobuf, by fulfilling the frontend.APIClient interface.
type FrontendAPI struct {
grpc *grpc.Server
cfg *viper.Viper
pool *redis.Pool
}
type frontendAPI FrontendAPI
// New returns an instantiated srvice
func New(cfg *viper.Viper, pool *redis.Pool) *FrontendAPI {
s := FrontendAPI{
pool: pool,
grpc: grpc.NewServer(grpc.StatsHandler(&ocgrpc.ServerHandler{})),
cfg: cfg,
}
// Add a hook to the logger to auto-count log lines for metrics output thru OpenCensus
log.AddHook(metrics.NewHook(FeLogLines, KeySeverity))
// Register gRPC server
frontend.RegisterAPIServer(s.grpc, (*frontendAPI)(&s))
feLog.Info("Successfully registered gRPC server")
return &s
}
// Open starts the api grpc service listening on the configured port.
func (s *FrontendAPI) Open() error {
ln, err := net.Listen("tcp", ":"+s.cfg.GetString("api.frontend.port"))
if err != nil {
feLog.WithFields(log.Fields{
"error": err.Error(),
"port": s.cfg.GetInt("api.frontend.port"),
}).Error("net.Listen() error")
return err
}
feLog.WithFields(log.Fields{"port": s.cfg.GetInt("api.frontend.port")}).Info("TCP net listener initialized")
go func() {
err := s.grpc.Serve(ln)
if err != nil {
feLog.WithFields(log.Fields{"error": err.Error()}).Error("gRPC serve() error")
}
feLog.Info("serving gRPC endpoints")
}()
return nil
}
// CreateRequest is this service's implementation of the CreateRequest gRPC method // defined in ../proto/frontend.proto
func (s *frontendAPI) CreateRequest(c context.Context, g *frontend.Group) (*frontend.Result, error) {
// Get redis connection from pool
redisConn := s.pool.Get()
defer redisConn.Close()
// Create context for tagging OpenCensus metrics.
funcName := "CreateRequest"
fnCtx, _ := tag.New(c, tag.Insert(KeyMethod, funcName))
// Write group
// TODO: Remove playerq module and just use redishelper module once
// indexing has its own implementation
err := playerq.Create(redisConn, g.Id, g.Properties)
if err != nil {
feLog.WithFields(log.Fields{
"error": err.Error(),
"component": "statestorage",
}).Error("State storage error")
stats.Record(fnCtx, FeGrpcErrors.M(1))
return &frontend.Result{Success: false, Error: err.Error()}, err
}
stats.Record(fnCtx, FeGrpcRequests.M(1))
return &frontend.Result{Success: true, Error: ""}, err
}
// DeleteRequest is this service's implementation of the DeleteRequest gRPC method defined in
// frontendapi/proto/frontend.proto
func (s *frontendAPI) DeleteRequest(c context.Context, g *frontend.Group) (*frontend.Result, error) {
// Get redis connection from pool
redisConn := s.pool.Get()
defer redisConn.Close()
// Create context for tagging OpenCensus metrics.
funcName := "DeleteRequest"
fnCtx, _ := tag.New(c, tag.Insert(KeyMethod, funcName))
// Write group
err := playerq.Delete(redisConn, g.Id)
if err != nil {
feLog.WithFields(log.Fields{
"error": err.Error(),
"component": "statestorage",
}).Error("State storage error")
stats.Record(fnCtx, FeGrpcErrors.M(1))
return &frontend.Result{Success: false, Error: err.Error()}, err
}
stats.Record(fnCtx, FeGrpcRequests.M(1))
return &frontend.Result{Success: true, Error: ""}, err
}
// GetAssignment is this service's implementation of the GetAssignment gRPC method defined in
// frontendapi/proto/frontend.proto
func (s *frontendAPI) GetAssignment(c context.Context, p *frontend.PlayerId) (*frontend.ConnectionInfo, error) {
// Get cancellable context
ctx, cancel := context.WithCancel(c)
defer cancel()
// Create context for tagging OpenCensus metrics.
funcName := "GetAssignment"
fnCtx, _ := tag.New(ctx, tag.Insert(KeyMethod, funcName))
// get and return connection string
var connString string
watchChan := s.watcher(ctx, s.pool, p.Id) // watcher() runs the appropriate Redis commands.
select {
case <-time.After(30 * time.Second): // TODO: Make this configurable.
err := errors.New("did not see matchmaking results in redis before timeout")
// TODO:Timeout: deal with the fallout
// When there is a timeout, need to send a stop to the watch channel.
// cancelling ctx isn't doing it.
//cancel()
feLog.WithFields(log.Fields{
"error": err.Error(),
"component": "statestorage",
"playerid": p.Id,
}).Error("State storage error")
errTag, _ := tag.NewKey("errtype")
fnCtx, _ := tag.New(ctx, tag.Insert(errTag, "watch_timeout"))
stats.Record(fnCtx, FeGrpcErrors.M(1))
return &frontend.ConnectionInfo{ConnectionString: ""}, err
case connString = <-watchChan:
feLog.Debug(p.Id, "connString:", connString)
}
stats.Record(fnCtx, FeGrpcRequests.M(1))
return &frontend.ConnectionInfo{ConnectionString: connString}, nil
}
// DeleteAssignment is this service's implementation of the DeleteAssignment gRPC method defined in
// frontendapi/proto/frontend.proto
func (s *frontendAPI) DeleteAssignment(c context.Context, p *frontend.PlayerId) (*frontend.Result, error) {
// Get redis connection from pool
redisConn := s.pool.Get()
defer redisConn.Close()
// Create context for tagging OpenCensus metrics.
funcName := "DeleteAssignment"
fnCtx, _ := tag.New(c, tag.Insert(KeyMethod, funcName))
// Write group
err := playerq.Delete(redisConn, p.Id)
if err != nil {
feLog.WithFields(log.Fields{
"error": err.Error(),
"component": "statestorage",
}).Error("State storage error")
stats.Record(fnCtx, FeGrpcErrors.M(1))
return &frontend.Result{Success: false, Error: err.Error()}, err
}
stats.Record(fnCtx, FeGrpcRequests.M(1))
return &frontend.Result{Success: true, Error: ""}, err
}
//TODO: Everything below this line will be moved to the redis statestorage library
// in an upcoming version.
// ================================================
// watcher makes a channel and returns it immediately. It also launches an
// asynchronous goroutine that watches a redis key and returns the value of
// the 'connstring' field of that key once it exists on the channel.
//
// The pattern for this function is from 'Go Concurrency Patterns', it is a function
// that wraps a closure goroutine, and returns a channel.
// reference: https://talks.golang.org/2012/concurrency.slide#25
func (s *frontendAPI) watcher(ctx context.Context, pool *redis.Pool, key string) <-chan string {
// Add the key as a field to all logs for the execution of this function.
feLog = feLog.WithFields(log.Fields{"key": key})
feLog.Debug("Watching key in statestorage for changes")
watchChan := make(chan string)
go func() {
// var declaration
var results string
var err = errors.New("haven't queried Redis yet")
// Loop, querying redis until this key has a value
for err != nil {
select {
case <-ctx.Done():
// Cleanup
close(watchChan)
return
default:
results, err = s.retrieveConnstring(ctx, pool, key, s.cfg.GetString("jsonkeys.connstring"))
if err != nil {
time.Sleep(5 * time.Second) // TODO: exp bo + jitter
}
}
}
// Return value retreived from Redis asynchonously and tell calling function we're done
feLog.Debug("Statestorage watched record update detected")
watchChan <- results
close(watchChan)
}()
return watchChan
}
// retrieveConnstring is a concurrent-safe, context-aware redis HGET of the 'connstring' fieldin the input key
// TODO: This will be moved to the redis statestorage module.
func (s *frontendAPI) retrieveConnstring(ctx context.Context, pool *redis.Pool, key string, field string) (string, error) {
// Add the key as a field to all logs for the execution of this function.
feLog = feLog.WithFields(log.Fields{"key": key})
cmd := "HGET"
feLog.WithFields(log.Fields{"query": cmd}).Debug("Statestorage operation")
// Get a connection to redis
redisConn, err := pool.GetContext(ctx)
defer redisConn.Close()
// Encountered an issue getting a connection from the pool.
if err != nil {
feLog.WithFields(log.Fields{
"error": err.Error(),
"query": cmd}).Error("Statestorage connection error")
return "", err
}
// Run redis query and return
return redis.String(redisConn.Do("HGET", key, field))
}

@ -1,7 +1,7 @@
/*
This application handles all the startup and connection scaffolding for
running a gRPC server serving the APIService as defined in
frontendapi/proto/frontend.pb.go
${OM_ROOT}/internal/pb/frontend.pb.go
All the actual important bits are in the API Server source code: apisrv/apisrv.go
@ -19,87 +19,13 @@ 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 main
import (
"errors"
"os"
"os/signal"
"github.com/GoogleCloudPlatform/open-match/cmd/frontendapi/apisrv"
"github.com/GoogleCloudPlatform/open-match/config"
"github.com/GoogleCloudPlatform/open-match/internal/metrics"
redishelpers "github.com/GoogleCloudPlatform/open-match/internal/statestorage/redis"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"go.opencensus.io/plugin/ocgrpc"
"github.com/GoogleCloudPlatform/open-match/internal/app/frontendapi"
)
var (
// Logrus structured logging setup
feLogFields = log.Fields{
"app": "openmatch",
"component": "frontend",
"caller": "frontendapi/main.go",
}
feLog = log.WithFields(feLogFields)
// Viper config management setup
cfg = viper.New()
err = errors.New("")
)
func init() {
// Logrus structured logging initialization
// Add a hook to the logger to auto-count log lines for metrics output thru OpenCensus
log.AddHook(metrics.NewHook(apisrv.FeLogLines, apisrv.KeySeverity))
// Viper config management initialization
cfg, err = config.Read()
if err != nil {
feLog.WithFields(log.Fields{
"error": err.Error(),
}).Error("Unable to load config file")
}
if cfg.GetBool("debug") == true {
log.SetLevel(log.DebugLevel) // debug only, verbose - turn off in production!
feLog.Warn("Debug logging configured. Not recommended for production!")
}
// Configure OpenCensus exporter to Prometheus
// metrics.ConfigureOpenCensusPrometheusExporter expects that every OpenCensus view you
// want to register is in an array, so append any views you want from other
// packages to a single array here.
ocServerViews := apisrv.DefaultFrontendAPIViews // FrontendAPI OpenCensus views.
ocServerViews = append(ocServerViews, ocgrpc.DefaultServerViews...) // gRPC OpenCensus views.
ocServerViews = append(ocServerViews, config.CfgVarCountView) // config loader view.
// Waiting on https://github.com/opencensus-integrations/redigo/pull/1
// ocServerViews = append(ocServerViews, redis.ObservabilityMetricViews...) // redis OpenCensus views.
feLog.WithFields(log.Fields{"viewscount": len(ocServerViews)}).Info("Loaded OpenCensus views")
metrics.ConfigureOpenCensusPrometheusExporter(cfg, ocServerViews)
}
func main() {
// Connect to redis
pool := redishelpers.ConnectionPool(cfg)
defer pool.Close()
// Instantiate the gRPC server with the connections we've made
feLog.WithFields(log.Fields{"testfield": "test"}).Info("Attempting to start gRPC server")
srv := apisrv.New(cfg, pool)
// Run the gRPC server
err := srv.Open()
if err != nil {
feLog.WithFields(log.Fields{"error": err.Error()}).Fatal("Failed to start gRPC server")
}
// Exit when we see a signal
terminate := make(chan os.Signal, 1)
signal.Notify(terminate, os.Interrupt)
<-terminate
feLog.Info("Shutting down gRPC server")
frontendapi.RunApplication()
}

@ -1 +0,0 @@
../../config/matchmaker_config.json

@ -1,4 +0,0 @@
/*
frontend is a package compiled from the protobuffer in <REPO_ROOT>/api/protobuf-spec/frontend.proto. It is auto-generated and shouldn't be edited.
*/
package frontend

@ -1,335 +0,0 @@
/*
Copyright 2018 Google LLC
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
https://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.
*/
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: frontend.proto
/*
Package frontend is a generated protocol buffer package.
It is generated from these files:
frontend.proto
It has these top-level messages:
Group
PlayerId
ConnectionInfo
Result
*/
package frontend
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// Data structure for a group of players to pass to the matchmaking function.
// Obviously, the group can be a group of one!
type Group struct {
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
Properties string `protobuf:"bytes,2,opt,name=properties" json:"properties,omitempty"`
}
func (m *Group) Reset() { *m = Group{} }
func (m *Group) String() string { return proto.CompactTextString(m) }
func (*Group) ProtoMessage() {}
func (*Group) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *Group) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *Group) GetProperties() string {
if m != nil {
return m.Properties
}
return ""
}
type PlayerId struct {
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
}
func (m *PlayerId) Reset() { *m = PlayerId{} }
func (m *PlayerId) String() string { return proto.CompactTextString(m) }
func (*PlayerId) ProtoMessage() {}
func (*PlayerId) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *PlayerId) GetId() string {
if m != nil {
return m.Id
}
return ""
}
// Simple message used to pass the connection string for the DGS to the player.
type ConnectionInfo struct {
ConnectionString string `protobuf:"bytes,1,opt,name=connection_string,json=connectionString" json:"connection_string,omitempty"`
}
func (m *ConnectionInfo) Reset() { *m = ConnectionInfo{} }
func (m *ConnectionInfo) String() string { return proto.CompactTextString(m) }
func (*ConnectionInfo) ProtoMessage() {}
func (*ConnectionInfo) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
func (m *ConnectionInfo) GetConnectionString() string {
if m != nil {
return m.ConnectionString
}
return ""
}
// Simple message to return success/failure and error status.
type Result struct {
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
Error string `protobuf:"bytes,2,opt,name=error" json:"error,omitempty"`
}
func (m *Result) Reset() { *m = Result{} }
func (m *Result) String() string { return proto.CompactTextString(m) }
func (*Result) ProtoMessage() {}
func (*Result) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
func (m *Result) GetSuccess() bool {
if m != nil {
return m.Success
}
return false
}
func (m *Result) GetError() string {
if m != nil {
return m.Error
}
return ""
}
func init() {
proto.RegisterType((*Group)(nil), "Group")
proto.RegisterType((*PlayerId)(nil), "PlayerId")
proto.RegisterType((*ConnectionInfo)(nil), "ConnectionInfo")
proto.RegisterType((*Result)(nil), "Result")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for API service
type APIClient interface {
CreateRequest(ctx context.Context, in *Group, opts ...grpc.CallOption) (*Result, error)
DeleteRequest(ctx context.Context, in *Group, opts ...grpc.CallOption) (*Result, error)
GetAssignment(ctx context.Context, in *PlayerId, opts ...grpc.CallOption) (*ConnectionInfo, error)
DeleteAssignment(ctx context.Context, in *PlayerId, opts ...grpc.CallOption) (*Result, error)
}
type aPIClient struct {
cc *grpc.ClientConn
}
func NewAPIClient(cc *grpc.ClientConn) APIClient {
return &aPIClient{cc}
}
func (c *aPIClient) CreateRequest(ctx context.Context, in *Group, opts ...grpc.CallOption) (*Result, error) {
out := new(Result)
err := grpc.Invoke(ctx, "/API/CreateRequest", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aPIClient) DeleteRequest(ctx context.Context, in *Group, opts ...grpc.CallOption) (*Result, error) {
out := new(Result)
err := grpc.Invoke(ctx, "/API/DeleteRequest", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aPIClient) GetAssignment(ctx context.Context, in *PlayerId, opts ...grpc.CallOption) (*ConnectionInfo, error) {
out := new(ConnectionInfo)
err := grpc.Invoke(ctx, "/API/GetAssignment", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aPIClient) DeleteAssignment(ctx context.Context, in *PlayerId, opts ...grpc.CallOption) (*Result, error) {
out := new(Result)
err := grpc.Invoke(ctx, "/API/DeleteAssignment", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for API service
type APIServer interface {
CreateRequest(context.Context, *Group) (*Result, error)
DeleteRequest(context.Context, *Group) (*Result, error)
GetAssignment(context.Context, *PlayerId) (*ConnectionInfo, error)
DeleteAssignment(context.Context, *PlayerId) (*Result, error)
}
func RegisterAPIServer(s *grpc.Server, srv APIServer) {
s.RegisterService(&_API_serviceDesc, srv)
}
func _API_CreateRequest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Group)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(APIServer).CreateRequest(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/API/CreateRequest",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(APIServer).CreateRequest(ctx, req.(*Group))
}
return interceptor(ctx, in, info, handler)
}
func _API_DeleteRequest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Group)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(APIServer).DeleteRequest(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/API/DeleteRequest",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(APIServer).DeleteRequest(ctx, req.(*Group))
}
return interceptor(ctx, in, info, handler)
}
func _API_GetAssignment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PlayerId)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(APIServer).GetAssignment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/API/GetAssignment",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(APIServer).GetAssignment(ctx, req.(*PlayerId))
}
return interceptor(ctx, in, info, handler)
}
func _API_DeleteAssignment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PlayerId)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(APIServer).DeleteAssignment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/API/DeleteAssignment",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(APIServer).DeleteAssignment(ctx, req.(*PlayerId))
}
return interceptor(ctx, in, info, handler)
}
var _API_serviceDesc = grpc.ServiceDesc{
ServiceName: "API",
HandlerType: (*APIServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CreateRequest",
Handler: _API_CreateRequest_Handler,
},
{
MethodName: "DeleteRequest",
Handler: _API_DeleteRequest_Handler,
},
{
MethodName: "GetAssignment",
Handler: _API_GetAssignment_Handler,
},
{
MethodName: "DeleteAssignment",
Handler: _API_DeleteAssignment_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "frontend.proto",
}
func init() { proto.RegisterFile("frontend.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 260 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x90, 0x41, 0x4b, 0xfb, 0x40,
0x10, 0xc5, 0x9b, 0xfc, 0x69, 0xda, 0x0e, 0x34, 0xff, 0xba, 0x78, 0x08, 0x39, 0x88, 0xec, 0xa9,
0x20, 0xee, 0x41, 0x0f, 0x7a, 0xf1, 0x50, 0x2a, 0x94, 0xdc, 0x4a, 0xfc, 0x00, 0x52, 0x93, 0x69,
0x59, 0x88, 0xbb, 0x71, 0x66, 0x72, 0xf0, 0x0b, 0xf9, 0x39, 0xc5, 0x4d, 0x6b, 0x55, 0xc4, 0xe3,
0xfb, 0xed, 0x7b, 0x8f, 0x7d, 0x03, 0xe9, 0x96, 0xbc, 0x13, 0x74, 0xb5, 0x69, 0xc9, 0x8b, 0xd7,
0x37, 0x30, 0x5c, 0x91, 0xef, 0x5a, 0x95, 0x42, 0x6c, 0xeb, 0x2c, 0x3a, 0x8f, 0xe6, 0x93, 0x32,
0xb6, 0xb5, 0x3a, 0x03, 0x68, 0xc9, 0xb7, 0x48, 0x62, 0x91, 0xb3, 0x38, 0xf0, 0x2f, 0x44, 0xe7,
0x30, 0x5e, 0x37, 0x9b, 0x57, 0xa4, 0xa2, 0xfe, 0x99, 0xd5, 0x77, 0x90, 0x2e, 0xbd, 0x73, 0x58,
0x89, 0xf5, 0xae, 0x70, 0x5b, 0xaf, 0x2e, 0xe0, 0xa4, 0xfa, 0x24, 0x8f, 0x2c, 0x64, 0xdd, 0x6e,
0x1f, 0x98, 0x1d, 0x1f, 0x1e, 0x02, 0xd7, 0xb7, 0x90, 0x94, 0xc8, 0x5d, 0x23, 0x2a, 0x83, 0x11,
0x77, 0x55, 0x85, 0xcc, 0xc1, 0x3c, 0x2e, 0x0f, 0x52, 0x9d, 0xc2, 0x10, 0x89, 0x3c, 0xed, 0x7f,
0xd6, 0x8b, 0xab, 0xb7, 0x08, 0xfe, 0x2d, 0xd6, 0x85, 0xd2, 0x30, 0x5d, 0x12, 0x6e, 0x04, 0x4b,
0x7c, 0xe9, 0x90, 0x45, 0x25, 0x26, 0xac, 0xcc, 0x47, 0xa6, 0x6f, 0xd6, 0x83, 0x0f, 0xcf, 0x3d,
0x36, 0xf8, 0xa7, 0xe7, 0x12, 0xa6, 0x2b, 0x94, 0x05, 0xb3, 0xdd, 0xb9, 0x67, 0x74, 0xa2, 0x26,
0xe6, 0x30, 0x3a, 0xff, 0x6f, 0xbe, 0x6f, 0xd4, 0x03, 0x35, 0x87, 0x59, 0x5f, 0xf9, 0x7b, 0xe2,
0x58, 0xfc, 0x94, 0x84, 0xeb, 0x5f, 0xbf, 0x07, 0x00, 0x00, 0xff, 0xff, 0x2b, 0xde, 0x2c, 0x5b,
0x8f, 0x01, 0x00, 0x00,
}

10
cmd/minimatch/Dockerfile Normal file

@ -0,0 +1,10 @@
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/minimatch/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/minimatch/minimatch .
ENTRYPOINT ["/minimatch"]

29
cmd/minimatch/main.go Normal file

@ -0,0 +1,29 @@
/*
This application is a minified version of Open Match.
All the actual important bits are in the API Server source code: apisrv/apisrv.go
Copyright 2019 Google LLC
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
https://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 main
import (
"github.com/GoogleCloudPlatform/open-match/internal/app/minimatch"
)
func main() {
minimatch.RunApplication()
}

@ -1,408 +0,0 @@
/*
Copyright 2018 Google LLC
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.
*/
// Note: the example only works with the code within the same release/branch.
// This is based on the example from the official k8s golang client repository:
// k8s.io/client-go/examples/create-update-delete-deployment/
package main
import (
"context"
"errors"
"os"
"strconv"
"strings"
"time"
"github.com/GoogleCloudPlatform/open-match/config"
"github.com/GoogleCloudPlatform/open-match/internal/metrics"
redisHelpers "github.com/GoogleCloudPlatform/open-match/internal/statestorage/redis"
"github.com/tidwall/gjson"
"go.opencensus.io/stats"
"go.opencensus.io/tag"
"github.com/gomodule/redigo/redis"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
batchv1 "k8s.io/api/batch/v1"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
//"k8s.io/kubernetes/pkg/api"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
// Uncomment the following line to load the gcp plugin (only required to authenticate against GKE clusters).
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
)
var (
// Logrus structured logging setup
mmforcLogFields = log.Fields{
"app": "openmatch",
"component": "mmforc",
"caller": "mmforc/main.go",
}
mmforcLog = log.WithFields(mmforcLogFields)
// Viper config management setup
cfg = viper.New()
err = errors.New("")
)
func init() {
// Logrus structured logging initialization
// Add a hook to the logger to auto-count log lines for metrics output thru OpenCensus
log.SetFormatter(&log.JSONFormatter{})
log.AddHook(metrics.NewHook(MmforcLogLines, KeySeverity))
// Viper config management initialization
cfg, err = config.Read()
if err != nil {
mmforcLog.WithFields(log.Fields{
"error": err.Error(),
}).Error("Unable to load config file")
}
if cfg.GetBool("debug") == true {
log.SetLevel(log.DebugLevel) // debug only, verbose - turn off in production!
mmforcLog.Warn("Debug logging configured. Not recommended for production!")
}
// Configure OpenCensus exporter to Prometheus
// metrics.ConfigureOpenCensusPrometheusExporter expects that every OpenCensus view you
// want to register is in an array, so append any views you want from other
// packages to a single array here.
ocMmforcViews := DefaultMmforcViews // mmforc OpenCensus views.
// Waiting on https://github.com/opencensus-integrations/redigo/pull/1
// ocMmforcViews = append(ocMmforcViews, redis.ObservabilityMetricViews...) // redis OpenCensus views.
mmforcLog.WithFields(log.Fields{"viewscount": len(ocMmforcViews)}).Info("Loaded OpenCensus views")
metrics.ConfigureOpenCensusPrometheusExporter(cfg, ocMmforcViews)
}
func main() {
pool := redisHelpers.ConnectionPool(cfg)
redisConn := pool.Get()
defer redisConn.Close()
// Get k8s credentials so we can starts k8s Jobs
mmforcLog.Info("Attempting to acquire k8s credentials")
config, err := rest.InClusterConfig()
if err != nil {
panic(err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err)
}
mmforcLog.Info("K8s credentials acquired")
start := time.Now()
checkProposals := true
// main loop; kick off matchmaker functions for profiles in the profile
// queue and an evaluator when proposals are in the proposals queue
for {
ctx, cancel := context.WithCancel(context.Background())
_ = cancel
// Get profiles and kick off a job for each
mmforcLog.WithFields(log.Fields{
"profileQueueName": cfg.GetString("queues.profiles.name"),
"pullCount": cfg.GetInt("queues.profiles.pullCount"),
"query": "SPOP",
"component": "statestorage",
}).Debug("Retreiving match profiles")
results, err := redis.Strings(redisConn.Do("SPOP",
cfg.GetString("queues.profiles.name"), cfg.GetInt("queues.profiles.pullCount")))
if err != nil {
panic(err)
}
if len(results) > 0 {
mmforcLog.WithFields(log.Fields{
"numProfiles": len(results),
}).Info("Starting MMF jobs...")
for _, profile := range results {
// Kick off the job asynchrnously
go mmfunc(ctx, profile, cfg, clientset, pool)
// Count the number of jobs running
redisHelpers.Increment(context.Background(), pool, "concurrentMMFs")
}
} else {
mmforcLog.WithFields(log.Fields{
"profileQueueName": cfg.GetString("queues.profiles.name"),
}).Info("Unable to retreive match profiles from statestorage - have you entered any?")
}
// Check to see if we should run the evaluator.
// Get number of running MMFs
r, err := redisHelpers.Retrieve(context.Background(), pool, "concurrentMMFs")
if err != nil {
if err.Error() == "redigo: nil returned" {
// No MMFs have run since we last evaluated; reset timer and loop
mmforcLog.Debug("Number of concurrentMMFs is nil")
start = time.Now()
time.Sleep(1000 * time.Millisecond)
}
continue
}
numRunning, err := strconv.Atoi(r)
if err != nil {
mmforcLog.WithFields(log.Fields{
"error": err.Error(),
}).Error("Issue retrieving number of currently running MMFs")
}
// We are ready to evaluate either when all MMFs are complete, or the
// timeout is reached.
//
// Tuning how frequently the evaluator runs is a complex topic and
// probably only of interest to users running large-scale production
// workloads with many concurrently running matchmaking functions,
// which have some overlap in the matchmaking player pools. Suffice to
// say that under load, this switch should almost always trigger the
// timeout interval code path. The concurrentMMFs check to see how
// many are still running is meant as a deadman's switch to prevent
// waiting to run the evaluator when all your MMFs are already
// finished.
switch {
case time.Since(start).Seconds() >= float64(cfg.GetInt("interval.evaluator")):
mmforcLog.WithFields(log.Fields{
"interval": cfg.GetInt("interval.evaluator"),
}).Info("Maximum evaluator interval exceeded")
checkProposals = true
// Opencensus tagging
ctx, _ = tag.New(ctx, tag.Insert(KeyEvalReason, "interval_exceeded"))
case numRunning <= 0:
mmforcLog.Info("All MMFs complete")
checkProposals = true
numRunning = 0
ctx, _ = tag.New(ctx, tag.Insert(KeyEvalReason, "mmfs_completed"))
}
if checkProposals {
// Make sure there are proposals in the queue. No need to run the
// evaluator if there are none.
checkProposals = false
mmforcLog.Info("Checking statestorage for match object proposals")
results, err := redisHelpers.Count(context.Background(), pool, cfg.GetString("queues.proposals.name"))
switch {
case err != nil:
mmforcLog.WithFields(log.Fields{
"error": err.Error(),
}).Error("Couldn't retrieve the length of the proposal queue from statestorage!")
case results == 0:
mmforcLog.WithFields(log.Fields{}).Warn("No proposals in the queue!")
default:
mmforcLog.WithFields(log.Fields{
"numProposals": results,
}).Info("Proposals available, evaluating!")
go evaluator(ctx, cfg, clientset)
}
_, err = redisHelpers.Delete(context.Background(), pool, "concurrentMMFs")
if err != nil {
mmforcLog.WithFields(log.Fields{
"error": err.Error(),
}).Error("Error deleting concurrent MMF counter!")
}
start = time.Now()
}
// TODO: Make this tunable via config.
// A sleep here is not critical but just a useful safety valve in case
// things are broken, to keep the main loop from going all-out and spamming the log.
mainSleep := 1000
mmforcLog.WithFields(log.Fields{
"ms": mainSleep,
}).Info("Sleeping...")
time.Sleep(time.Duration(mainSleep) * time.Millisecond)
} // End main for loop
}
// mmfunc generates a k8s job that runs the specified mmf container image.
// resultsID is the redis key that the Backend API is monitoring for results; we can 'short circuit' and write errors directly to this key if we can't run the MMF for some reason.
func mmfunc(ctx context.Context, resultsID string, cfg *viper.Viper, clientset *kubernetes.Clientset, pool *redis.Pool) {
// Generate the various keys/names, some of which must be populated to the k8s job.
imageName := cfg.GetString("defaultImages.mmf.name") + ":" + cfg.GetString("defaultImages.mmf.tag")
jobType := "mmf"
ids := strings.Split(resultsID, ".") // comes in as dot-concatinated moID and profID.
moID := ids[0]
profID := ids[1]
timestamp := strconv.Itoa(int(time.Now().Unix()))
jobName := timestamp + "." + moID + "." + profID + "." + jobType
propID := "proposal." + timestamp + "." + moID + "." + profID
// Extra fields for structured logging
lf := log.Fields{"jobName": jobName}
if cfg.GetBool("debug") { // Log a lot more info.
lf = log.Fields{
"jobType": jobType,
"backendMatchObject": moID,
"profile": profID,
"jobTimestamp": timestamp,
"containerImage": imageName,
"jobName": jobName,
"profileImageJSONKey": cfg.GetString("jsonkeys.mmfImage"),
}
}
mmfuncLog := mmforcLog.WithFields(lf)
// Read the full profile from redis and access any keys that are important to deciding how MMFs are run.
// TODO: convert this to using redispb and directly access the protobuf message instead of retrieving as a map?
profile, err := redisHelpers.RetrieveAll(ctx, pool, profID)
if err != nil {
// Log failure to read this profile and return - won't run an MMF for an unreadable profile.
mmfuncLog.WithFields(log.Fields{"error": err.Error()}).Error("Failure retreiving profile from statestorage")
return
}
// Got profile from state storage, make sure it is valid
if gjson.Valid(profile["properties"]) {
profileImage := gjson.Get(profile["properties"], cfg.GetString("jsonkeys.mmfImage"))
if profileImage.Exists() {
imageName = profileImage.String()
mmfuncLog = mmfuncLog.WithFields(log.Fields{"containerImage": imageName})
} else {
mmfuncLog.Warn("Failed to read image name from profile at configured json key, using default image instead")
}
}
mmfuncLog.Info("Attempting to create mmf k8s job")
// Kick off k8s job
envvars := []apiv1.EnvVar{
{Name: "MMF_PROFILE_ID", Value: profID},
{Name: "MMF_PROPOSAL_ID", Value: propID},
{Name: "MMF_REQUEST_ID", Value: moID},
{Name: "MMF_ERROR_ID", Value: resultsID},
{Name: "MMF_TIMESTAMP", Value: timestamp},
}
err = submitJob(clientset, jobType, jobName, imageName, envvars)
if err != nil {
// Record failure & log
stats.Record(ctx, mmforcMmfFailures.M(1))
mmfuncLog.WithFields(log.Fields{"error": err.Error()}).Error("MMF job submission failure!")
} else {
// Record Success
stats.Record(ctx, mmforcMmfs.M(1))
}
}
// evaluator generates a k8s job that runs the specified evaluator container image.
func evaluator(ctx context.Context, cfg *viper.Viper, clientset *kubernetes.Clientset) {
imageName := cfg.GetString("defaultImages.evaluator.name") + ":" + cfg.GetString("defaultImages.evaluator.tag")
// Generate the job name
timestamp := strconv.Itoa(int(time.Now().Unix()))
jobType := "evaluator"
jobName := timestamp + "." + jobType
mmforcLog.WithFields(log.Fields{
"jobName": jobName,
"containerImage": imageName,
}).Info("Attempting to create evaluator k8s job")
// Kick off k8s job
envvars := []apiv1.EnvVar{{Name: "MMF_TIMESTAMP", Value: timestamp}}
err = submitJob(clientset, jobType, jobName, imageName, envvars)
if err != nil {
// Record failure & log
stats.Record(ctx, mmforcEvalFailures.M(1))
mmforcLog.WithFields(log.Fields{
"error": err.Error(),
"jobName": jobName,
"containerImage": imageName,
}).Error("Evaluator job submission failure!")
} else {
// Record success
stats.Record(ctx, mmforcEvals.M(1))
}
}
// submitJob submits a job to kubernetes
func submitJob(clientset *kubernetes.Clientset, jobType string, jobName string, imageName string, envvars []apiv1.EnvVar) error {
// DEPRECATED: will be removed in a future vrsion. Please switch to using the 'MMF_*' environment variables.
v := strings.Split(jobName, ".")
envvars = append(envvars, apiv1.EnvVar{Name: "PROFILE", Value: strings.Join(v[:len(v)-1], ".")})
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: jobName,
},
Spec: batchv1.JobSpec{
Completions: int32Ptr(1),
Template: apiv1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": jobType,
},
Annotations: map[string]string{
// Unused; here as an example.
// Later we can put things more complicated than
// env vars here and read them using k8s downward API
// volumes
"profile": jobName,
},
},
Spec: apiv1.PodSpec{
RestartPolicy: "Never",
Containers: []apiv1.Container{
{
Name: jobType,
Image: imageName,
ImagePullPolicy: "Always",
Env: envvars,
},
},
},
},
},
}
// Get the namespace for the job from the current namespace, otherwise, use default
namespace := os.Getenv("METADATA_NAMESPACE")
if len(namespace) == 0 {
namespace = apiv1.NamespaceDefault
}
// Submit kubernetes job
jobsClient := clientset.BatchV1().Jobs(namespace)
result, err := jobsClient.Create(job)
if err != nil {
// TODO: replace queued profiles if things go south
mmforcLog.WithFields(log.Fields{
"error": err.Error(),
}).Error("Couldn't create k8s job!")
}
mmforcLog.WithFields(log.Fields{
"jobName": result.GetObjectMeta().GetName(),
}).Info("Created job.")
return err
}
// readability functions used by generateJobSpec
func int32Ptr(i int32) *int32 { return &i }
func strPtr(i string) *string { return &i }

@ -1 +0,0 @@
../../config/matchmaker_config.json

23
cmd/mmlogicapi/Dockerfile Normal file

@ -0,0 +1,23 @@
# Copyright 2019 Google LLC
#
# 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.
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/mmlogicapi/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/mmlogicapi/mmlogicapi .
ENTRYPOINT ["/mmlogicapi"]

@ -1,7 +1,7 @@
/*
This application handles all the startup and connection scaffolding for
running a gRPC server serving the APIService as defined in
mmlogic/proto/mmlogic.pb.go
${OM_ROOT}/internal/pb/mmlogic.pb.go
All the actual important bits are in the API Server source code: apisrv/apisrv.go
@ -22,84 +22,9 @@ limitations under the License.
package main
import (
"errors"
"os"
"os/signal"
"github.com/GoogleCloudPlatform/open-match/cmd/mmlogicapi/apisrv"
"github.com/GoogleCloudPlatform/open-match/config"
"github.com/GoogleCloudPlatform/open-match/internal/metrics"
redisHelpers "github.com/GoogleCloudPlatform/open-match/internal/statestorage/redis"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"go.opencensus.io/plugin/ocgrpc"
"github.com/GoogleCloudPlatform/open-match/internal/app/mmlogicapi"
)
var (
// Logrus structured logging setup
mlLogFields = log.Fields{
"app": "openmatch",
"component": "mmlogic",
"caller": "mmlogicapi/main.go",
}
mlLog = log.WithFields(mlLogFields)
// Viper config management setup
cfg = viper.New()
err = errors.New("")
)
func init() {
// Logrus structured logging initialization
// Add a hook to the logger to auto-count log lines for metrics output thru OpenCensus
log.AddHook(metrics.NewHook(apisrv.MlLogLines, apisrv.KeySeverity))
// Viper config management initialization
cfg, err = config.Read()
if err != nil {
mlLog.WithFields(log.Fields{
"error": err.Error(),
}).Error("Unable to load config file")
}
if cfg.GetBool("debug") == true {
log.SetLevel(log.DebugLevel) // debug only, verbose - turn off in production!
mlLog.Warn("Debug logging configured. Not recommended for production!")
}
// Configure OpenCensus exporter to Prometheus
// metrics.ConfigureOpenCensusPrometheusExporter expects that every OpenCensus view you
// want to register is in an array, so append any views you want from other
// packages to a single array here.
ocServerViews := apisrv.DefaultMmlogicAPIViews // Matchmaking logic API OpenCensus views.
ocServerViews = append(ocServerViews, ocgrpc.DefaultServerViews...) // gRPC OpenCensus views.
ocServerViews = append(ocServerViews, config.CfgVarCountView) // config loader view.
// Waiting on https://github.com/opencensus-integrations/redigo/pull/1
// ocServerViews = append(ocServerViews, redis.ObservabilityMetricViews...) // redis OpenCensus views.
mlLog.WithFields(log.Fields{"viewscount": len(ocServerViews)}).Info("Loaded OpenCensus views")
metrics.ConfigureOpenCensusPrometheusExporter(cfg, ocServerViews)
}
func main() {
// Connect to redis
pool := redisHelpers.ConnectionPool(cfg)
defer pool.Close()
// Instantiate the gRPC server with the connections we've made
mlLog.WithFields(log.Fields{"testfield": "test"}).Info("Attempting to start gRPC server")
srv := apisrv.New(cfg, pool)
// Run the gRPC server
err := srv.Open()
if err != nil {
mlLog.WithFields(log.Fields{"error": err.Error()}).Fatal("Failed to start gRPC server")
}
// Exit when we see a signal
terminate := make(chan os.Signal, 1)
signal.Notify(terminate, os.Interrupt)
<-terminate
mlLog.Info("Shutting down gRPC server")
mmlogicapi.RunApplication()
}

@ -1 +0,0 @@
../../config/matchmaker_config.json

@ -18,6 +18,7 @@ limitations under the License.
package config
import (
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"go.opencensus.io/stats"
@ -29,7 +30,6 @@ var (
logFields = log.Fields{
"app": "openmatch",
"component": "config",
"caller": "config/main.go",
}
cfgLog = log.WithFields(logFields)
@ -42,18 +42,22 @@ var (
// REDIS_SENTINEL_PORT_6379_TCP_PORT=6379
// REDIS_SENTINEL_PORT_6379_TCP_PROTO=tcp
// REDIS_SENTINEL_SERVICE_HOST=10.55.253.195
//
// MMFs are expected to get their configuation from env vars instead
// of reading the config file. So, config parameters that are required
// by MMFs should be populated to env vars.
envMappings = map[string]string{
"redis.hostname": "REDIS_SENTINEL_SERVICE_HOST",
"redis.port": "REDIS_SENTINEL_SERVICE_PORT",
"redis.user": "REDIS_USER",
"redis.password": "REDIS_PASSWORD",
"redis.hostname": "REDIS_SERVICE_HOST",
"redis.port": "REDIS_SERVICE_PORT",
"redis.pool.maxIdle": "REDIS_POOL_MAXIDLE",
"redis.pool.maxActive": "REDIS_POOL_MAXACTIVE",
"redis.pool.idleTimeout": "REDIS_POOL_IDLETIMEOUT",
"debug": "DEBUG",
"api.mmlogic.hostname": "OM_MMLOGICAPI_SERVICE_HOST",
"api.mmlogic.port": "OM_MMLOGICAPI_SERVICE_PORT",
}
// Viper config management setup
cfg = viper.New()
// OpenCensus
cfgVarCount = stats.Int64("config/vars_total", "Number of config vars read during initialization", "1")
// CfgVarCountView is the Open Census view for the cfgVarCount measure.
@ -67,12 +71,16 @@ var (
// Read reads a config file into a viper.Viper instance and associates environment vars defined in
// config.envMappings
func Read() (*viper.Viper, error) {
func Read() (View, error) {
cfg := viper.New()
// Viper config management initialization
// Support either json or yaml file types (json for backwards compatibility
// with previous versions)
cfg.SetConfigType("json")
cfg.SetConfigType("yaml")
cfg.SetConfigName("matchmaker_config")
cfg.AddConfigPath(".")
cfg.AddConfigPath("config")
// Read in config file using Viper
err := cfg.ReadInConfig()
@ -104,10 +112,21 @@ func Read() (*viper.Viper, error) {
"envvar": envVar,
"module": "config",
}).Info("Binding environment var as a config variable")
}
}
// Look for updates to the config; in Kubernetes, this is implemented using
// a ConfigMap that is written to the matchmaker_config.yaml file, which is
// what the Open Match components using Viper monitor for changes.
// More details about Open Match's use of Kubernetes ConfigMaps at:
// https://github.com/GoogleCloudPlatform/open-match/issues/42
cfg.WatchConfig() // Watch and re-read config file.
// Write a log when the configuration changes.
cfg.OnConfigChange(func(event fsnotify.Event) {
cfgLog.WithFields(log.Fields{
"filename": event.Name,
"operation": event.Op,
}).Info("Server configuration changed.")
})
return cfg, err
}

33
config/config_test.go Normal file

@ -0,0 +1,33 @@
/*
Package config contains convenience functions for reading and managing configuration.
Copyright 2019 Google LLC
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 config
import (
"testing"
)
func TestReadConfig(t *testing.T) {
cfg, err := Read()
if err != nil {
t.Fatalf("cannot load config, %s", err)
}
if cfg.GetString("metrics.endpoint") != "/metrics" {
t.Errorf("av.GetString('metrics.endpoint') = %s, expected '/metrics'", cfg.GetString("metrics.endpoint"))
}
}

@ -1,101 +0,0 @@
{
"debug": true,
"api": {
"backend": {
"hostname": "om-backendapi",
"port": 50505
},
"frontend": {
"hostname": "om-frontendapi",
"port": 50504
},
"mmlogic": {
"hostname": "om-mmlogicapi",
"port": 50503
}
},
"metrics": {
"port": 9555,
"endpoint": "/metrics",
"reportingPeriod": 5
},
"queues": {
"profiles": {
"name": "profileq",
"pullCount": 100
},
"proposals": {
"name": "proposalq"
}
},
"ignoreLists": {
"proposed": {
"name": "proposed",
"offset": 0,
"duration": 800
},
"deindexed": {
"name": "deindexed",
"offset": 0,
"duration": 800
},
"expired": {
"name": "timestamp",
"offset": 800,
"duration": 0
}
},
"defaultImages": {
"evaluator": {
"name": "gcr.io/matchmaker-dev-201405/openmatch-evaluator",
"tag": "dev"
},
"mmf": {
"name": "gcr.io/matchmaker-dev-201405/openmatch-mmf",
"tag": "py3"
}
},
"redis": {
"user": "",
"password": "",
"pool" : {
"maxIdle" : 3,
"maxActive" : 0,
"idleTimeout" : 60
},
"queryArgs":{
"count": 10000
},
"results": {
"pageSize": 10000
}
},
"jsonkeys": {
"mmfImage": "imagename",
"rosters": "properties.rosters",
"connstring": "connstring",
"pools": "properties.pools"
},
"interval": {
"evaluator": 10,
"resultsTimeout": 30
},
"playerIndices": [
"char.cleric",
"char.knight",
"char.paladin",
"map.aleroth",
"map.oasis",
"mmr.rating",
"mode.battleroyale",
"mode.ctf",
"region.europe-east1",
"region.europe-west1",
"region.europe-west2",
"region.europe-west3",
"region.europe-west4",
"role.dps",
"role.support",
"role.tank"
]
}

@ -0,0 +1,90 @@
# kubectl create configmap om-configmap --from-file=config/matchmaker_config.yaml
debug: true
logging:
level: debug
format: text
source: false
api:
backend:
hostname: om-backendapi
port: 50505
backoff: "[2 32] *2 ~0.33 <30"
proxyport: 51505
frontend:
hostname: om-frontendapi
port: 50504
backoff: "[2 32] *2 ~0.33 <300"
proxyport: 51504
mmlogic:
hostname: om-mmlogicapi
port: 50503
proxyport: 51503
functions:
port: 50502
proxyport: 51502
evaluator:
# Evaluator intervals are in milliseconds
pollIntervalMs: 1000
maxWaitMs: 10000
metrics:
port: 9555
endpoint: /metrics
reportingPeriod: 5
queues:
proposals:
name: proposalq
ignoreLists:
proposed:
name: proposed
offset: 0
duration: 800
deindexed:
name: deindexed
offset: 0
duration: 800
expired:
name: OM_METADATA.accessed
offset: 800
duration: 0
redis:
pool:
maxIdle: 3
maxActive: 0
idleTimeout: 60
queryArgs:
count: 10000
results:
pageSize: 10000
expirations:
player: 43200
matchobject: 43200
jsonkeys:
rosters: properties.rosters
pools: properties.pools
playerIndices:
- char.cleric
- char.knight
- char.paladin
- map.aleroth
- map.oasis
- mmr.rating
- mode.battleroyale
- mode.ctf
- mode.demo
- region.europe-east1
- region.europe-west1
- region.europe-west2
- region.europe-west3
- region.europe-west4
- role.dps
- role.support
- role.tank

46
config/view.go Normal file

@ -0,0 +1,46 @@
/*
Package config contains convenience functions for reading and managing configuration.
Copyright 2019 Google LLC
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 config
import (
"time"
"github.com/spf13/viper"
)
// View is a read-only view of the Open Match configuration.
// New accessors from Viper should be added here.
type View interface {
IsSet(string) bool
GetString(string) string
GetInt(string) int
GetInt64(string) int64
GetStringSlice(string) []string
GetBool(string) bool
GetDuration(string) time.Duration
GetStringMap(string) map[string]interface{}
}
// Sub returns a subset of configuration filtered by the key.
func Sub(v View, key string) View {
vcfg, ok := v.(*viper.Viper)
if ok {
return vcfg.Sub(key)
}
return nil
}

50
config/view_test.go Normal file

@ -0,0 +1,50 @@
/*
Package config contains convenience functions for reading and managing configuration.
Copyright 2019 Google LLC
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 config
import (
"testing"
"github.com/spf13/viper"
)
func TestSubFromViper(t *testing.T) {
v := viper.New()
v.Set("a", "a")
v.Set("b", "b")
v.Set("c", "c")
v.Set("a.a", "a.a")
v.Set("a.b", "a.b")
av := Sub(v, "a")
if av == nil {
t.Fatalf("Sub(%v, 'a') => %v", v, av)
}
if av.GetString("a") != "a.a" {
t.Errorf("av.GetString('a') = %s, expected 'a.a'", av.GetString("a"))
}
if av.GetString("a.a") != "" {
t.Errorf("av.GetString('a.a') = %s, expected ''", av.GetString("a.a"))
}
if av.GetString("b") != "a.b" {
t.Errorf("av.GetString('b') = %s, expected 'a.b'", av.GetString("b"))
}
if av.GetString("c") != "" {
t.Errorf("av.GetString('c') = %s, expected ''", av.GetString(""))
}
}

@ -1,53 +0,0 @@
{
"apiVersion":"extensions/v1beta1",
"kind":"Deployment",
"metadata":{
"name":"om-backendapi",
"labels":{
"app":"openmatch",
"component": "backend"
}
},
"spec":{
"replicas":1,
"selector":{
"matchLabels":{
"app":"openmatch",
"component": "backend"
}
},
"template":{
"metadata":{
"labels":{
"app":"openmatch",
"component": "backend"
}
},
"spec":{
"containers":[
{
"name":"om-backend",
"image":"gcr.io/matchmaker-dev-201405/openmatch-backendapi:dev",
"imagePullPolicy":"Always",
"ports": [
{
"name": "grpc",
"containerPort": 50505
},
{
"name": "metrics",
"containerPort": 9555
}
],
"resources":{
"requests":{
"memory":"100Mi",
"cpu":"100m"
}
}
}
]
}
}
}
}

@ -1,20 +0,0 @@
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "om-backendapi"
},
"spec": {
"selector": {
"app": "openmatch",
"component": "backend"
},
"ports": [
{
"protocol": "TCP",
"port": 50505,
"targetPort": "grpc"
}
]
}
}

@ -1,53 +0,0 @@
{
"apiVersion":"extensions/v1beta1",
"kind":"Deployment",
"metadata":{
"name":"om-frontendapi",
"labels":{
"app":"openmatch",
"component": "frontend"
}
},
"spec":{
"replicas":1,
"selector":{
"matchLabels":{
"app":"openmatch",
"component": "frontend"
}
},
"template":{
"metadata":{
"labels":{
"app":"openmatch",
"component": "frontend"
}
},
"spec":{
"containers":[
{
"name":"om-frontendapi",
"image":"gcr.io/matchmaker-dev-201405/openmatch-frontendapi:dev",
"imagePullPolicy":"Always",
"ports": [
{
"name": "grpc",
"containerPort": 50504
},
{
"name": "metrics",
"containerPort": 9555
}
],
"resources":{
"requests":{
"memory":"100Mi",
"cpu":"100m"
}
}
}
]
}
}
}
}

@ -1,20 +0,0 @@
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "om-frontendapi"
},
"spec": {
"selector": {
"app": "openmatch",
"component": "frontend"
},
"ports": [
{
"protocol": "TCP",
"port": 50504,
"targetPort": "grpc"
}
]
}
}

@ -1,27 +0,0 @@
{
"apiVersion": "monitoring.coreos.com/v1",
"kind": "ServiceMonitor",
"metadata": {
"name": "openmatch-metrics",
"labels": {
"app": "openmatch",
"agent": "opencensus",
"destination": "prometheus"
}
},
"spec": {
"selector": {
"matchLabels": {
"app": "openmatch",
"agent": "opencensus",
"destination": "prometheus"
}
},
"endpoints": [
{
"port": "metrics",
"interval": "10s"
}
]
}
}

@ -1,78 +0,0 @@
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "om-frontend-metrics",
"labels": {
"app": "openmatch",
"component": "frontend",
"agent": "opencensus",
"destination": "prometheus"
}
},
"spec": {
"selector": {
"app": "openmatch",
"component": "frontend"
},
"ports": [
{
"name": "metrics",
"targetPort": 9555,
"port": 19555
}
]
}
}
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "om-backend-metrics",
"labels": {
"app": "openmatch",
"component": "backend",
"agent": "opencensus",
"destination": "prometheus"
}
},
"spec": {
"selector": {
"app": "openmatch",
"component": "backend"
},
"ports": [
{
"name": "metrics",
"targetPort": 9555,
"port": 29555
}
]
}
}
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "om-mmforc-metrics",
"labels": {
"app": "openmatch",
"component": "mmforc",
"agent": "opencensus",
"destination": "prometheus"
}
},
"spec": {
"selector": {
"app": "openmatch",
"component": "mmforc"
},
"ports": [
{
"name": "metrics",
"targetPort": 9555,
"port": 39555
}
]
}
}

@ -1,59 +0,0 @@
{
"apiVersion":"extensions/v1beta1",
"kind":"Deployment",
"metadata":{
"name":"om-mmforc",
"labels":{
"app":"openmatch",
"component": "mmforc"
}
},
"spec":{
"replicas":1,
"selector":{
"matchLabels":{
"app":"openmatch",
"component": "mmforc"
}
},
"template":{
"metadata":{
"labels":{
"app":"openmatch",
"component": "mmforc"
}
},
"spec":{
"containers":[
{
"name":"om-mmforc",
"image":"gcr.io/matchmaker-dev-201405/openmatch-mmforc:dev",
"imagePullPolicy":"Always",
"ports": [
{
"name": "metrics",
"containerPort":9555
}
],
"resources":{
"requests":{
"memory":"100Mi",
"cpu":"100m"
}
},
"env":[
{
"name":"METADATA_NAMESPACE",
"valueFrom": {
"fieldRef": {
"fieldPath": "metadata.namespace"
}
}
}
]
}
]
}
}
}
}

@ -1,19 +0,0 @@
{
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
"kind": "ClusterRoleBinding",
"metadata": {
"name": "mmf-sa"
},
"subjects": [
{
"kind": "ServiceAccount",
"name": "default",
"namespace": "default"
}
],
"roleRef": {
"kind": "ClusterRole",
"name": "cluster-admin",
"apiGroup": "rbac.authorization.k8s.io"
}
}

@ -1,53 +0,0 @@
{
"apiVersion":"extensions/v1beta1",
"kind":"Deployment",
"metadata":{
"name":"om-mmlogicapi",
"labels":{
"app":"openmatch",
"component": "mmlogic"
}
},
"spec":{
"replicas":1,
"selector":{
"matchLabels":{
"app":"openmatch",
"component": "mmlogic"
}
},
"template":{
"metadata":{
"labels":{
"app":"openmatch",
"component": "mmlogic"
}
},
"spec":{
"containers":[
{
"name":"om-mmlogic",
"image":"gcr.io/matchmaker-dev-201405/openmatch-mmlogicapi:dev",
"imagePullPolicy":"Always",
"ports": [
{
"name": "grpc",
"containerPort": 50503
},
{
"name": "metrics",
"containerPort": 9555
}
],
"resources":{
"requests":{
"memory":"100Mi",
"cpu":"100m"
}
}
}
]
}
}
}
}

@ -1,20 +0,0 @@
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "om-mmlogicapi"
},
"spec": {
"selector": {
"app": "openmatch",
"component": "mmlogic"
},
"ports": [
{
"protocol": "TCP",
"port": 50503,
"targetPort": "grpc"
}
]
}
}

@ -1,20 +0,0 @@
{
"apiVersion": "monitoring.coreos.com/v1",
"kind": "Prometheus",
"metadata": {
"name": "prometheus"
},
"spec": {
"serviceMonitorSelector": {
"matchLabels": {
"app": "openmatch"
}
},
"serviceAccountName": "prometheus",
"resources": {
"requests": {
"memory": "400Mi"
}
}
}
}

@ -1,266 +0,0 @@
{
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
"kind": "ClusterRoleBinding",
"metadata": {
"name": "prometheus-operator"
},
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"name": "prometheus-operator"
},
"subjects": [
{
"kind": "ServiceAccount",
"name": "prometheus-operator",
"namespace": "default"
}
]
}
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "prometheus"
}
}
{
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
"kind": "ClusterRole",
"metadata": {
"name": "prometheus"
},
"rules": [
{
"apiGroups": [
""
],
"resources": [
"nodes",
"services",
"endpoints",
"pods"
],
"verbs": [
"get",
"list",
"watch"
]
},
{
"apiGroups": [
""
],
"resources": [
"configmaps"
],
"verbs": [
"get"
]
},
{
"nonResourceURLs": [
"/metrics"
],
"verbs": [
"get"
]
}
]
}
{
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
"kind": "ClusterRoleBinding",
"metadata": {
"name": "prometheus"
},
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"name": "prometheus"
},
"subjects": [
{
"kind": "ServiceAccount",
"name": "prometheus",
"namespace": "default"
}
]
}
{
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
"kind": "ClusterRole",
"metadata": {
"name": "prometheus-operator"
},
"rules": [
{
"apiGroups": [
"extensions"
],
"resources": [
"thirdpartyresources"
],
"verbs": [
"*"
]
},
{
"apiGroups": [
"apiextensions.k8s.io"
],
"resources": [
"customresourcedefinitions"
],
"verbs": [
"*"
]
},
{
"apiGroups": [
"monitoring.coreos.com"
],
"resources": [
"alertmanagers",
"prometheuses",
"prometheuses/finalizers",
"servicemonitors"
],
"verbs": [
"*"
]
},
{
"apiGroups": [
"apps"
],
"resources": [
"statefulsets"
],
"verbs": [
"*"
]
},
{
"apiGroups": [
""
],
"resources": [
"configmaps",
"secrets"
],
"verbs": [
"*"
]
},
{
"apiGroups": [
""
],
"resources": [
"pods"
],
"verbs": [
"list",
"delete"
]
},
{
"apiGroups": [
""
],
"resources": [
"services",
"endpoints"
],
"verbs": [
"get",
"create",
"update"
]
},
{
"apiGroups": [
""
],
"resources": [
"nodes"
],
"verbs": [
"list",
"watch"
]
},
{
"apiGroups": [
""
],
"resources": [
"namespaces"
],
"verbs": [
"list"
]
}
]
}
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "prometheus-operator"
}
}
{
"apiVersion": "extensions/v1beta1",
"kind": "Deployment",
"metadata": {
"labels": {
"k8s-app": "prometheus-operator"
},
"name": "prometheus-operator"
},
"spec": {
"replicas": 1,
"template": {
"metadata": {
"labels": {
"k8s-app": "prometheus-operator"
}
},
"spec": {
"containers": [
{
"args": [
"--kubelet-service=kube-system/kubelet",
"--config-reloader-image=quay.io/coreos/configmap-reload:v0.0.1"
],
"image": "quay.io/coreos/prometheus-operator:v0.17.0",
"name": "prometheus-operator",
"ports": [
{
"containerPort": 8080,
"name": "http"
}
],
"resources": {
"limits": {
"cpu": "200m",
"memory": "100Mi"
},
"requests": {
"cpu": "100m",
"memory": "50Mi"
}
}
}
],
"securityContext": {
"runAsNonRoot": true,
"runAsUser": 65534
},
"serviceAccountName": "prometheus-operator"
}
}
}
}

@ -1,22 +0,0 @@
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "prometheus"
},
"spec": {
"type": "NodePort",
"ports": [
{
"name": "web",
"nodePort": 30900,
"port": 9090,
"protocol": "TCP",
"targetPort": "web"
}
],
"selector": {
"prometheus": "prometheus"
}
}
}

@ -1,38 +0,0 @@
{
"apiVersion": "extensions/v1beta1",
"kind": "Deployment",
"metadata": {
"name": "redis-master"
},
"spec": {
"selector": {
"matchLabels": {
"app": "mm",
"tier": "storage"
}
},
"replicas": 1,
"template": {
"metadata": {
"labels": {
"app": "mm",
"tier": "storage"
}
},
"spec": {
"containers": [
{
"name": "redis-master",
"image": "redis:4.0.11",
"ports": [
{
"name": "redis",
"containerPort": 6379
}
]
}
]
}
}
}
}

@ -1,20 +0,0 @@
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "redis-sentinel"
},
"spec": {
"selector": {
"app": "mm",
"tier": "storage"
},
"ports": [
{
"protocol": "TCP",
"port": 6379,
"targetPort": "redis"
}
]
}
}

18
doc.go Normal file

@ -0,0 +1,18 @@
/*
* Copyright 2019 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 openmatch provides flexible, extensible, and scalable video game matchmaking.
package openmatch

84
docs/building.md Normal file

@ -0,0 +1,84 @@
## Building
Documentation and usage guides on how to set up and customize Open Match.
### Precompiled container images
Once we reach a 1.0 release, we plan to produce publicly available (Linux) Docker container images of major releases in a public image registry. Until then, refer to the 'Compiling from source' section below.
### Compiling from source
The easiest way to build Open Match is to use the Makefile. Before you can use the Makefile make sure you have the following dependencies:
```bash
# Install Open Match Toolchain Dependencies (Debian other OSes including Mac OS X have similar dependencies)
sudo apt-get update; sudo apt-get install -y -q python3 python3-virtualenv virtualenv make google-cloud-sdk git unzip tar
# Setup your repository like Go workspace, https://golang.org/doc/code.html#Workspaces
# This requirement will go away soon.
mkdir -p workspace/src/github.com/GoogleCloudPlatform/
cd workspace/src/github.com/GoogleCloudPlatform/
export GOPATH=$HOME/workspace
export GO111MODULE=on
git clone https://github.com/GoogleCloudPlatform/open-match.git
cd open-match
```
[Docker](https://docs.docker.com/install/) and [Go 1.11+](https://golang.org/dl/) is also required. If your distro is new enough you can probably run `sudo apt-get install -y golang` or download the newest version from https://golang.org/.
To build all the artifacts of Open Match you can simply run the following commands.
```bash
# Downloads all the tools needed to build Open Match
make install-toolchain
# Generates protocol buffer code files
make all-protos
# Builds all the binaries
make all
# Builds all the images.
make build-images
```
Once build you can use a command like `docker images` to see all the images that were build.
Before creating a pull request you can run `make local-cloud-build` to simulate a Cloud Build run to check for regressions.
The directory structure is a typical Go structure so if you do the following you should be able to work on this project within your IDE.
```bash
cd $GOPATH
mkdir -p src/github.com/GoogleCloudPlatform/
cd src/github.com/GoogleCloudPlatform/
# If you're going to contribute you'll want to fork open-match, see CONTRIBUTING.md for details.
git clone https://github.com/GoogleCloudPlatform/open-match.git
cd open-match
# Open IDE in this directory.
```
Lastly, this project uses go modules so you'll want to set `export GO111MODULE=on` before building.
## Zero to Open Match
To deploy Open Match quickly to a Kubernetes cluster run these commands.
```bash
# Downloads all the tools.
make install-toolchain
# Create a GKE Cluster
make create-gke-cluster
# OR Create a Minikube Cluster
make create-mini-cluster
# Install Helm
make push-helm
# Build and push images
make push-images -j4
# Deploy Open Match with example functions
make install-chart install-example-chart
```
## Docker Image Builds
All the core components for Open Match are written in Golang and use the [Dockerfile multistage builder pattern](https://docs.docker.com/develop/develop-images/multistage-build/). This pattern uses intermediate Docker containers as a Golang build environment while producing lightweight, minimized container images as final build artifacts. When the project is ready for production, we will modify the `Dockerfile`s to uncomment the last build stage. Although this pattern is great for production container images, it removes most of the utilities required to troubleshoot issues during development.
## Configuration
Currently, each component reads a local config file `matchmaker_config.json`, and all components assume they have the same configuration. To this end, there is a single centralized config file located in the `<REPO_ROOT>/config/` which is symlinked to each component's subdirectory for convenience when building locally. When `docker build`ing the component container images, the Dockerfile copies the centralized config file into the component directory.
We plan to replace this with a Kubernetes-managed config with dynamic reloading, please join the discussion in [Issue #42](issues/42).

136
docs/concepts.md Normal file

@ -0,0 +1,136 @@
# Core Concepts
[Watch the introduction of Open Match at Unite Berlin 2018 on YouTube](https://youtu.be/qasAmy_ko2o)
Open Match is designed to support massively concurrent matchmaking, and to be scalable to player populations of hundreds of millions or more. It attempts to apply stateless web tech microservices patterns to game matchmaking. If you're not sure what that means, that's okay &mdash; it is fully open source and designed to be customizable to fit into your online game architecture &mdash; so have a look a the code and modify it as you see fit.
## Glossary
### General
* **DGS** &mdash; Dedicated game server
* **Client** &mdash; The game client program the player uses when playing the game
* **Session** &mdash; In Open Match, players are matched together, then assigned to a server which hosts the game _session_. Depending on context, this may be referred to as a _match_, _map_, or just _game_ elsewhere in the industry.
### Open Match
* **Component** &mdash; One of the discrete processes in an Open Match deployment. Open Match is composed of multiple scalable microservices called _components_.
* **State Storage** &mdash; The storage software used by Open Match to hold all the matchmaking state. Open Match ships with [Redis](https://redis.io/) as the default state storage.
* **MMFOrc** &mdash; Matchmaker function orchestrator. This Open Match core component is in charge of kicking off custom matchmaking functions (MMFs) and evaluator processes.
* **MMF** &mdash; Matchmaking function. This is the customizable matchmaking logic.
* **MMLogic API** &mdash; An API that provides MMF SDK functionality. It is optional - you can also do all the state storage read and write operations yourself if you have a good reason to do so.
* **Director** &mdash; The software you (as a developer) write against the Open Match Backend API. The _Director_ decides which MMFs to run, and is responsible for sending MMF results to a DGS to host the session.
### Data Model
* **Player** &mdash; An ID and list of attributes with values for a player who wants to participate in matchmaking.
* **Roster** &mdash; A list of player objects. Used to hold all the players on a single team.
* **Filter** &mdash; A _filter_ is used to narrow down the players to only those who have an attribute value within a certain integer range. All attributes are integer values in Open Match because [that is how indices are implemented](internal/statestorage/redis/playerindices/playerindices.go). A _filter_ is defined in a _player pool_.
* **Player Pool** &mdash; A list of all the players who fit all the _filters_ defined in the pool.
* **Match Object** &mdash; A protobuffer message format that contains the _profile_ and the results of the matchmaking function. Sent to the backend API from your game backend with the _roster_(s) empty and then returned from your MMF with the matchmaking results filled in.
* **Profile** &mdash; The json blob containing all the parameters used by your MMF to select which players go into a roster together.
* **Assignment** &mdash; Refers to assigning a player or group of players to a dedicated game server instance. Open Match offers a path to send dedicated game server connection details from your backend to your game clients after a match has been made.
* **Ignore List** &mdash; Removing players from matchmaking consideration is accomplished using _ignore lists_. They contain lists of player IDs that your MMF should not include when making matches.
## Requirements
* [Kubernetes](https://kubernetes.io/) cluster &mdash; tested with version 1.11.7.
* [Redis 4+](https://redis.io/) &mdash; tested with 4.0.11.
* Open Match is compiled against the latest release of [Golang](https://golang.org/) &mdash; tested with 1.11.5.
## Components
Open Match is a set of processes designed to run on Kubernetes. It contains these **core** components:
1. Frontend API
1. Backend API
1. Matchmaker Function Orchestrator (MMFOrc) (may be deprecated in future versions)
It includes these **optional** (but recommended) components:
1. Matchmaking Logic (MMLogic) API
It also explicitly depends on these two **customizable** components.
1. Matchmaking "Function" (MMF)
1. Evaluator (may be optional in future versions)
While **core** components are fully open source and _can_ be modified, they are designed to support the majority of matchmaking scenarios *without need to change the source code*. The Open Match repository ships with simple **customizable** MMF and Evaluator examples, but it is expected that most users will want full control over the logic in these, so they have been designed to be as easy to modify or replace as possible.
### Frontend API
The Frontend API accepts the player data and puts it in state storage so your Matchmaking Function (MMF) can access it.
The Frontend API is a server application that implements the [gRPC](https://grpc.io/) service defined in `api/protobuf-spec/frontend.proto`. At the most basic level, it expects clients to connect and send:
* A **unique ID** for the group of players (the group can contain any number of players, including only one).
* A **json blob** containing all player-related data you want to use in your matchmaking function.
The client is expected to maintain a connection, waiting for an update from the API that contains the details required to connect to a dedicated game server instance (an 'assignment'). There are also basic functions for removing an ID from the matchmaking pool or an existing match.
### Backend API
The Backend API writes match objects to state storage which the Matchmaking Functions (MMFs) access to decide which players should be matched. It returns the results from those MMFs.
The Backend API is a server application that implements the [gRPC](https://grpc.io/) service defined in `api/protobuf-spec/backend.proto`. At the most basic level, it expects to be connected to your online infrastructure (probably to your server scaling manager or **director**, or even directly to a dedicated game server), and to receive:
* A **unique ID** for a matchmaking profile.
* A **json blob** containing all the matching-related data and filters you want to use in your matchmaking function.
* An optional list of **roster**s to hold the resulting teams chosen by your matchmaking function.
* An optional set of **filters** that define player pools your matchmaking function will choose players from.
Your game backend is expected to maintain a connection, waiting for 'filled' match objects containing a roster of players. The Backend API also provides a return path for your game backend to return dedicated game server connection details (an 'assignment') to the game client, and to delete these 'assignments'.
### Matchmaking Function Orchestrator (MMFOrc)
The MMFOrc kicks off your custom matchmaking function (MMF) for every unique profile submitted to the Backend API in a match object. It also runs the Evaluator to resolve conflicts in case more than one of your profiles matched the same players.
The MMFOrc exists to orchestrate/schedule your **custom components**, running them as often as required to meet the demands of your game. MMFOrc runs in an endless loop, submitting MMFs and Evaluator jobs to Kubernetes.
### Matchmaking Logic (MMLogic) API
The MMLogic API provides a series of gRPC functions that act as a Matchmaking Function SDK. Much of the basic, boilerplate code for an MMF is the same regardless of what players you want to match together. The MMLogic API offers a gRPC interface for many common MMF tasks, such as:
1. Reading a profile from state storage.
1. Running filters on players in state strorage. It automatically removes players on ignore lists as well!
1. Removing chosen players from consideration by other MMFs (by adding them to an ignore list). It does it automatically for you when writing your results!
1. Writing the matchmaking results to state storage.
1. (Optional, NYI) Exporting MMF stats for metrics collection.
More details about the available gRPC calls can be found in the [API Specification](api/protobuf-spec/messages.proto).
**Note**: using the MMLogic API is **optional**. It tries to simplify the development of MMFs, but if you want to take care of these tasks on your own, you can make few or no calls to the MMLogic API as long as your MMF still completes all the required tasks. Read the [Matchmaking Functions section](#matchmaking-functions-mmfs) for more details of what work an MMF must do.
### Evaluator
The Evaluator resolves conflicts when multiple MMFs select the same player(s).
The Evaluator is a component run by the Matchmaker Function Orchestrator (MMFOrc) after the matchmaker functions have been run, and some proposed results are available. The Evaluator looks at all the proposals, and if multiple proposals contain the same player(s), it breaks the tie. In many simple matchmaking setups with only a few game modes and well-tuned matchmaking functions, the Evaluator may functionally be a no-op or first-in-first-out algorithm. In complex matchmaking setups where, for example, a player can queue for multiple types of matches, the Evaluator provides the critical customizability to evaluate all available proposals and approve those that will passed to your game servers.
Large-scale concurrent matchmaking functions is a complex topic, and users who wish to do this are encouraged to engage with the [Open Match community](https://github.com/GoogleCloudPlatform/open-match#get-involved) about patterns and best practices.
### Matchmaking Functions (MMFs)
Matchmaking Functions (MMFs) are run by the Matchmaker Function Orchestrator (MMFOrc) &mdash; once per profile it sees in state storage. The MMF is run as a Job in Kubernetes, and has full access to read and write from state storage. At a high level, the encouraged pattern is to write a MMF in whatever language you are comfortable in that can do the following things:
- [x] Be packaged in a (Linux) Docker container.
- [x] Read/write from the Open Match state storage &mdash; Open Match ships with Redis as the default state storage.
- [x] Read a profile you wrote to state storage using the Backend API.
- [x] Select from the player data you wrote to state storage using the Frontend API. It must respect all the ignore lists defined in the matchmaker config.
- [ ] Run your custom logic to try to find a match.
- [x] Write the match object it creates to state storage at a specified key.
- [x] Remove the players it selected from consideration by other MMFs by adding them to the appropriate ignore list.
- [x] Notify the MMFOrc of completion.
- [x] (Optional, but recommended) Export stats for metrics collection.
**Open Match offers [matchmaking logic API](#matchmaking-logic-mmlogic-api) calls for handling the checked items, as long as you are able to format your input and output in the data schema Open Match expects (defined in the [protobuf messages](api/protobuf-spec/messages.proto)).** You can to do this work yourself if you don't want to or can't use the data schema Open Match is looking for. However, the data formats expected by Open Match are pretty generalized and will work with most common matchmaking scenarios and game types. If you have questions about how to fit your data into the formats specified, feel free to ask us in the [Slack or mailing group](#get-involved).
Example MMFs are provided in these languages:
- [C#](examples/functions/csharp/simple) (doesn't use the MMLogic API)
- [Python3](examples/functions/python3/mmlogic-simple) (MMLogic API enabled)
- [PHP](examples/functions/php/mmlogic-simple) (MMLogic API enabled)
- [golang](examples/functions/golang/manual-simple) (doesn't use the MMLogic API)
## Additional examples
**Note:** These examples will be expanded on in future releases.
The following examples of how to call the APIs are provided in the repository. Both have a `Dockerfile` and `cloudbuild.yaml` files in their respective directories:
* `test/cmd/frontendclient/main.go` acts as a client to the the Frontend API, putting a player into the queue with simulated latencies from major metropolitan cities and a couple of other matchmaking attributes. It then waits for you to manually put a value in Redis to simulate a server connection string being written using the backend API 'CreateAssignments' call, and displays that value on stdout for you to verify.
* `examples/backendclient/main.go` calls the Backend API and passes in the profile found in `backendstub/profiles/testprofile.json` to the `ListMatches` API endpoint, then continually prints the results until you exit, or there are insufficient players to make a match based on the profile..

@ -1,21 +1,61 @@
# Development Guide
This doc explains how to setup a development environment so you can get started contributing to Open Match. If you instead want to write a matchmaker that _uses_ Open Match, you probably want to read the [User Guide](user_guide.md).
# Compiling from source
All components of Open Match produce (Linux) Docker container images as artifacts, and there are included `Dockerfile`s for each. [Google Cloud Platform Cloud Build](https://cloud.google.com/cloud-build/docs/) users will also find `cloudbuild_<name>.yaml` files for each component in the repository root.
All components of Open Match produce (Linux) Docker container images as artifacts, and there are included `Dockerfile`s for each. [Google Cloud Platform Cloud Build](https://cloud.google.com/cloud-build/docs/) users will also find `cloudbuild.yaml` files for each component in their respective directories. Note that most of them build from an 'base' image called `openmatch-devbase`. You can find a `Dockerfile` and `cloudbuild_base.yaml` file for this in the repository root. Build it first!
Note: Although Google Cloud Platform includes some free usage, you may incur charges following this guide if you use GCP products.
**This project has not completed a first-line security audit, and there are definitely going to be some service accounts that are too permissive. This should be fine for testing/development in a local environment, but absolutely should not be used as-is in a production environment.**
## Security Disclaimer
**This project has not completed a first-line security audit, and there are definitely going to be some service accounts that are too permissive. This should be fine for testing/development in a local environment, but absolutely should not be used as-is in a production environment without your team/organization evaluating it's permissions.**
## Before getting started
**NOTE**: Before starting with this guide, you'll need to update all the URIs from the tutorial's gcr.io container image registry with the URI for your own image registry. If you are using the gcr.io registry on GCP, the default URI is `gcr.io/<PROJECT_NAME>`. Here's an example command in Linux to do the replacement for you this (replace YOUR_REGISTRY_URI with your URI, this should be run from the repository root directory):
```
# Linux
egrep -lR 'matchmaker-dev-201405' . | xargs sed -i -e 's|matchmaker-dev-201405|<PROJECT_NAME>|g'
```
```
# Mac OS, you can delete the .backup files after if all looks good
egrep -lR 'matchmaker-dev-201405' . | xargs sed -i'.backup' -e 's|matchmaker-dev-201405|<PROJECT_NAME>|g'
```
## Example of building using Google Cloud Builder
The [Quickstart for Docker](https://cloud.google.com/cloud-build/docs/quickstart-docker) guide explains how to set up a project, enable billing, enable Cloud Build, and install the Cloud SDK if you haven't do these things before. Once you get to 'Preparing source files' you are ready to continue with the steps below.
* Clone this repo to a local machine or Google Cloud Shell session, and cd into it.
* Run the following one-line bash script to compile all the images for the first time, and push them to your gcr.io registry. You must enable the [Container Registry API](https://console.cloud.google.com/flows/enableapi?apiid=containerregistry.googleapis.com) first.
```
for dfile in $(ls Dockerfile.*); do gcloud builds submit --config cloudbuild_${dfile##*.}.yaml; done
```
* In Linux, you can run the following one-line bash script to compile all the images for the first time, and push them to your gcr.io registry. You must enable the [Container Registry API](https://console.cloud.google.com/flows/enableapi?apiid=containerregistry.googleapis.com) first.
```
# First, build the 'base' image. Some other images depend on this so it must complete first.
gcloud builds submit --config cloudbuild_base.yaml
# Build all other images.
for dfile in $(find . -name "Dockerfile" -iregex "./\(cmd\|test\|examples\)/.*"); do cd $(dirname ${dfile}); gcloud builds submit --config cloudbuild.yaml & cd -; done
```
Note: as of v0.3.0 alpha, the Python and PHP MMF examples still depend on the previous way of building until [issue #42, introducing new config management](https://github.com/GoogleCloudPlatform/open-match/issues/42) is resolved (apologies for the inconvenience):
```
gcloud builds submit --config cloudbuild_mmf_py3.yaml
gcloud builds submit --config cloudbuild_mmf_php.yaml
```
* Once the cloud builds have completed, you can verify that all the builds succeeded in the cloud console or by by checking the list of images in your **gcr.io** registry:
```
gcloud container images list
```
(your registry name will be different)
```
NAME
gcr.io/matchmaker-dev-201405/openmatch-backendapi
gcr.io/matchmaker-dev-201405/openmatch-devbase
gcr.io/matchmaker-dev-201405/openmatch-evaluator
gcr.io/matchmaker-dev-201405/openmatch-frontendapi
gcr.io/matchmaker-dev-201405/openmatch-mmf-golang-manual-simple
gcr.io/matchmaker-dev-201405/openmatch-mmf-php-mmlogic-simple
gcr.io/matchmaker-dev-201405/openmatch-mmf-py3-mmlogic-simple
gcr.io/matchmaker-dev-201405/openmatch-mmforc
gcr.io/matchmaker-dev-201405/openmatch-mmlogicapi
```
## Example of starting a GKE cluster
A cluster with mostly default settings will work for this development guide. In the Cloud SDK command below we start it with machines that have 4 vCPUs. Alternatively, you can use the 'Create Cluster' button in [Google Cloud Console]("https://console.cloud.google.com/kubernetes").
@ -32,78 +72,115 @@ gcloud compute zones list
## Configuration
Currently, each component reads a local config file `matchmaker_config.json`, and all components assume they have the same configuration. To this end, there is a single centralized config file located in the `<REPO_ROOT>/config/` which is symlinked to each component's subdirectory for convenience when building locally.
**NOTE** 'defaultImages' container images names in the config file will need to be updated with **your container registry URI**. Here's an example command in Linux to do this (just replace YOUR_REGISTRY_URI with the appropriate location in your environment, should be run from the config directory):
```
sed -i 's|gcr.io/matchmaker-dev-201405|YOUR_REGISTRY_URI|g' matchmaker_config.json
```
For MacOS the `-i` flag creates backup files when changing the original file in place. You can use the following command, and then delete the `*.backup` files afterwards if you don't need them anymore:
```
sed -i'.backup' -e 's|gcr.io/matchmaker-dev-201405|YOUR_REGISTRY_URI|g' matchmaker_config.json
```
If you are using the gcr.io registry on GCP, the default URI is `gcr.io/<PROJECT_NAME>`.
We plan to replace this with a Kubernetes-managed config with dynamic reloading when development time allows. Pull requests are welcome!
Currently, each component reads a local config file `matchmaker_config.json`, and all components assume they have the same configuration (if you would like to help us design the replacement config solution, please join the [discussion](https://github.com/GoogleCloudPlatform/open-match/issues/42). To this end, there is a single centralized config file located in the `<REPO_ROOT>/config/` which is symlinked to each component's subdirectory for convenience when building locally. Note: [there is an issue with symlinks on Windows](../issues/57).
## Running Open Match in a development environment
The rest of this guide assumes you have a cluster (example is using GKE, but works on any cluster with a little tweaking), and kubectl configured to administer that cluster, and you've built all the Docker container images described by `Dockerfiles` in the repository root directory and given them the docker tag 'dev'. It assumes you are in the `<REPO_ROOT>/deployments/k8s/` directory.
**NOTE** Kubernetes resources that use container images will need to be updated with **your container registry URI**. Here's an example command in Linux to do this (just replace YOUR_REGISTRY_URI with the appropriate location in your environment):
```
sed -i 's|gcr.io/matchmaker-dev-201405|YOUR_REGISTRY_URI|g' *deployment.json
```
For MacOS the `-i` flag creates backup files when changing the original file in place. You can use the following command, and then delete the `*.backup` files afterwards if you don't need them anymore:
```
sed -i'.backup' -e 's|gcr.io/matchmaker-dev-201405|YOUR_REGISTRY_URI|g' *deployment.json
```
If you are using the gcr.io registry on GCP, the default URI is `gcr.io/<PROJECT_NAME>`.
* Start a copy of redis and a service in front of it:
```
kubectl apply -f redis_deployment.json
kubectl apply -f redis_service.json
```
```
kubectl apply -f redis_deployment.yaml
kubectl apply -f redis_service.yaml
```
* Run the **core components**: the frontend API, the backend API, the matchmaker function orchestrator (MMFOrc), and the matchmaking logic API.
**NOTE** In order to kick off jobs, the matchmaker function orchestrator needs a service account with permission to administer the cluster. This should be updated to have min required perms before launch, this is pretty permissive but acceptable for closed testing:
```
kubectl apply -f backendapi_deployment.json
kubectl apply -f backendapi_service.json
kubectl apply -f frontendapi_deployment.json
kubectl apply -f frontendapi_service.json
kubectl apply -f mmforc_deployment.json
kubectl apply -f mmforc_serviceaccount.json
kubectl apply -f mmlogic_deployment.json
kubectl apply -f mmlogic_service.json
```
```
kubectl apply -f backendapi_deployment.yaml
kubectl apply -f backendapi_service.yaml
kubectl apply -f frontendapi_deployment.yaml
kubectl apply -f frontendapi_service.yaml
kubectl apply -f mmforc_deployment.yaml
kubectl apply -f mmforc_serviceaccount.yaml
kubectl apply -f mmlogicapi_deployment.yaml
kubectl apply -f mmlogicapi_service.yaml
```
* [optional, but recommended] Configure the OpenCensus metrics services:
```
kubectl apply -f metrics_services.json
```
```
kubectl apply -f metrics_services.yaml
```
* [optional] Trying to apply the Kubernetes Prometheus Operator resource definition files without a cluster-admin rolebinding on GKE doesn't work without running the following command first. See https://github.com/coreos/prometheus-operator/issues/357
```
kubectl create clusterrolebinding projectowner-cluster-admin-binding --clusterrole=cluster-admin --user=<GCP_ACCOUNT>
```
```
kubectl create clusterrolebinding projectowner-cluster-admin-binding --clusterrole=cluster-admin --user=<GCP_ACCOUNT>
```
* [optional, uses beta software] If using Prometheus as your metrics gathering backend, configure the [Prometheus Kubernetes Operator](https://github.com/coreos/prometheus-operator):
```
kubectl apply -f prometheus_operator.json
kubectl apply -f prometheus.json
kubectl apply -f prometheus_service.json
kubectl apply -f metrics_servicemonitor.json
```
```
kubectl apply -f prometheus_operator.yaml
kubectl apply -f prometheus.yaml
kubectl apply -f prometheus_service.yaml
kubectl apply -f metrics_servicemonitor.yaml
```
You should now be able to see the core component pods running using a `kubectl get pods`, and the core component metrics in the Prometheus Web UI by running `kubectl proxy <PROMETHEUS_POD_NAME> 9090:9090` in your local shell, then opening http://localhost:9090/targets in your browser to see which services Prometheus is collecting from.
Here's an example output from `kubectl get all` if everything started correctly, and you included all the optional components (note: this could become out-of-date with upcoming versions; apologies if that happens):
```
NAME READY STATUS RESTARTS AGE
pod/om-backendapi-84bc9d8fff-q89kr 1/1 Running 0 9m
pod/om-frontendapi-55d5bb7946-c5ccb 1/1 Running 0 9m
pod/om-mmforc-85bfd7f4f6-wmwhc 1/1 Running 0 9m
pod/om-mmlogicapi-6488bc7fc6-g74dm 1/1 Running 0 9m
pod/prometheus-operator-5c8774cdd8-7c5qm 1/1 Running 0 9m
pod/prometheus-prometheus-0 2/2 Running 0 9m
pod/redis-master-9b6b86c46-b7ggn 1/1 Running 0 9m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.59.240.1 <none> 443/TCP 19m
service/om-backend-metrics ClusterIP 10.59.254.43 <none> 29555/TCP 9m
service/om-backendapi ClusterIP 10.59.240.211 <none> 50505/TCP 9m
service/om-frontend-metrics ClusterIP 10.59.246.228 <none> 19555/TCP 9m
service/om-frontendapi ClusterIP 10.59.250.59 <none> 50504/TCP 9m
service/om-mmforc-metrics ClusterIP 10.59.240.59 <none> 39555/TCP 9m
service/om-mmlogicapi ClusterIP 10.59.248.3 <none> 50503/TCP 9m
service/prometheus NodePort 10.59.252.212 <none> 9090:30900/TCP 9m
service/prometheus-operated ClusterIP None <none> 9090/TCP 9m
service/redis ClusterIP 10.59.249.197 <none> 6379/TCP 9m
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
deployment.extensions/om-backendapi 1 1 1 1 9m
deployment.extensions/om-frontendapi 1 1 1 1 9m
deployment.extensions/om-mmforc 1 1 1 1 9m
deployment.extensions/om-mmlogicapi 1 1 1 1 9m
deployment.extensions/prometheus-operator 1 1 1 1 9m
deployment.extensions/redis-master 1 1 1 1 9m
NAME DESIRED CURRENT READY AGE
replicaset.extensions/om-backendapi-84bc9d8fff 1 1 1 9m
replicaset.extensions/om-frontendapi-55d5bb7946 1 1 1 9m
replicaset.extensions/om-mmforc-85bfd7f4f6 1 1 1 9m
replicaset.extensions/om-mmlogicapi-6488bc7fc6 1 1 1 9m
replicaset.extensions/prometheus-operator-5c8774cdd8 1 1 1 9m
replicaset.extensions/redis-master-9b6b86c46 1 1 1 9m
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
deployment.apps/om-backendapi 1 1 1 1 9m
deployment.apps/om-frontendapi 1 1 1 1 9m
deployment.apps/om-mmforc 1 1 1 1 9m
deployment.apps/om-mmlogicapi 1 1 1 1 9m
deployment.apps/prometheus-operator 1 1 1 1 9m
deployment.apps/redis-master 1 1 1 1 9m
NAME DESIRED CURRENT READY AGE
replicaset.apps/om-backendapi-84bc9d8fff 1 1 1 9m
replicaset.apps/om-frontendapi-55d5bb7946 1 1 1 9m
replicaset.apps/om-mmforc-85bfd7f4f6 1 1 1 9m
replicaset.apps/om-mmlogicapi-6488bc7fc6 1 1 1 9m
replicaset.apps/prometheus-operator-5c8774cdd8 1 1 1 9m
replicaset.apps/redis-master-9b6b86c46 1 1 1 9m
NAME DESIRED CURRENT AGE
statefulset.apps/prometheus-prometheus 1 1 9m
```
### End-to-End testing
**Note** The programs provided below are just bare-bones manual testing programs with no automation and no claim of code coverage. This sparseness of this part of the documentation is because we expect to discard all of these tools and write a fully automated end-to-end test suite and a collection of load testing tools, with extensive stats output and tracing capabilities before 1.0 release. Tracing has to be integrated first, which will be in an upcoming release.
**Note**: The programs provided below are just bare-bones manual testing programs with no automation and no claim of code coverage. This sparseness of this part of the documentation is because we expect to discard all of these tools and write a fully automated end-to-end test suite and a collection of load testing tools, with extensive stats output and tracing capabilities before 1.0 release. Tracing has to be integrated first, which will be in an upcoming release.
In the end: *caveat emptor*. These tools all work and are quite small, and as such are fairly easy for developers to understand by looking at the code and logging output. They are provided as-is just as a reference point of how to begin experimenting with Open Match integrations.
In the end: *caveat emptor*. These tools all work and are quite small, and as such are fairly easy for developers to understand by looking at the code and logging output. They are provided as-is just as a reference point of how to begin experimenting with Open Match integrations.
* `examples/frontendclient` is a fake client for the Frontend API. It pretends to be a real game client connecting to Open Match and requests a game, then dumps out the connection string it receives. Note that it doesn't actually test the return path by looking for arbitrary results from your matchmaking function; it pauses and tells you the name of a key to set a connection string in directly using a redis-cli client.
* `examples/backendclient` is a fake client for the Backend API. It pretends to be a dedicated game server backend connecting to openmatch and sending in a match profile to fill. Once it receives a match object with a roster, it will also issue a call to assign the player IDs, and gives an example connection string. If it never seems to get a match, make sure you're adding players to the pool using the other two tools. Note: building this image requires that you first build the 'base' dev image (look for `cloudbuild_base.yaml` and `Dockerfile.base` in the root directory) and then update the first step to point to that image in your registry. This will be simplified in a future release.
* `test/cmd/client` is a (VERY) basic client load simulation tool. It does **not** test the Frontend API - in fact, it ignores it and writes players directly to state storage on its own. It doesn't do anything but loop endlessly, writing players into state storage so you can test your backend integration, and run your custom MMFs and Evaluators (which are only triggered when there are players in the pool).
* `test/cmd/frontendclient/` is a fake client for the Frontend API. It pretends to be group of real game clients connecting to Open Match. It requests a game, then dumps out the results each player receives to the screen until you press the enter key. **Note**: If you're using the rest of these test programs, you're probably using the Backend Client below. The default profiles that command sends to the backend look for many more than one player, so if you want to see meaningful results from running this Frontend Client, you're going to need to generate a bunch of fake players using the client load simulation tool at the same time. Otherwise, expect to wait until it times out as your matchmaker never has enough players to make a successful match.
* `examples/backendclient` is a fake client for the Backend API. It pretends to be a dedicated game server backend connecting to openmatch and sending in a match profile to fill. Once it receives a match object with a roster, it will also issue a call to assign the player IDs, and gives an example connection string. If it never seems to get a match, make sure you're adding players to the pool using the other two tools. Note: building this image requires that you first build the 'base' dev image (look for `cloudbuild_base.yaml` and `Dockerfile.base` in the root directory) and then update the first step to point to that image in your registry. This will be simplified in a future release. **Note**: If you run this by itself, expect it to wait about 30 seconds, then return a result of 'insufficient players' and exit - this is working as intended. Use the client load simulation tool below to add players to the pool or you'll never be able to make a successful match.
* `test/cmd/clientloadgen/` is a (VERY) basic client load simulation tool. It does **not** test the Frontend API - in fact, it ignores it and writes players directly to state storage on its own. It doesn't do anything but loop endlessly, writing players into state storage so you can test your backend integration, and run your custom MMFs and Evaluators (which are only triggered when there are players in the pool).
### Resources

@ -0,0 +1,61 @@
# v{version}
This is the {version} release of Open Match.
Check the [README](https://github.com/GoogleCloudPlatform/open-match/tree/release-{version}) for details on features, installation and usage.
Release Notes
-------------
{ insert enhancements from the changelog and/or security and breaking changes }
**Breaking Changes**
* API Changed #PR
**Enhancements**
* New Harness #PR
**Security Fixes**
* Reduced privileges required for MMF. #PR
See [CHANGELOG](https://github.com/GoogleCloudPlatform/open-match/blob/release-{version}/CHANGELOG.md) for more details on changes.
Images
------
```bash
# Servers
docker pull gcr.io/open-match-public-images/openmatch-backendapi:{version}
docker pull gcr.io/open-match-public-images/openmatch-frontendapi:{version}
docker pull gcr.io/open-match-public-images/openmatch-mmforc:{version}
docker pull gcr.io/open-match-public-images/openmatch-mmlogicapi:{version}
# Evaluators
docker pull gcr.io/open-match-public-images/openmatch-evaluator-serving:{version}
# Sample Match Making Functions
docker pull gcr.io/open-match-public-images/openmatch-mmf-go-grpc-serving-simple:{version}
# Test Clients
docker pull gcr.io/open-match-public-images/openmatch-backendclient:{version}
docker pull gcr.io/open-match-public-images/openmatch-clientloadgen:{version}
docker pull gcr.io/open-match-public-images/openmatch-frontendclient:{version}
```
_This software is currently alpha, and subject to change. Not to be used in production systems._
Installation
------------
To deploy Open Match in your Kubernetes cluster run the following commands:
```bash
# Grant yourself cluster-admin permissions so that you can deploy service accounts.
kubectl create clusterrolebinding myname-cluster-admin-binding --clusterrole=cluster-admin --user=$(YOUR_KUBERNETES_USER_NAME)
# Place all Open Match components in their own namespace.
kubectl create namespace open-match
# Install Open Match and monitoring services.
kubectl apply -f https://github.com/GoogleCloudPlatform/open-match/releases/download/v{version}/install.yaml --namespace open-match
# Install the example MMF and Evaluator.
kubectl apply -f https://github.com/GoogleCloudPlatform/open-match/releases/download/v{version}/install-example.yaml --namespace open-match
```

@ -0,0 +1,27 @@
#!/bin/bash
# Usage:
# ./release.sh 0.5.0-82d034f unstable
# ./release.sh [SOURCE VERSION] [DEST VERSION]
# This is a basic shell script to publish the latest Open Match Images
# There's no guardrails yet so use with care.
# Purge Images
# docker rmi $(docker images -a -q)
# 0.4.0-82d034f
SOURCE_VERSION=$1
DEST_VERSION=$2
SOURCE_PROJECT_ID=open-match-build
DEST_PROJECT_ID=open-match-public-images
IMAGE_NAMES="openmatch-backendapi openmatch-frontendapi openmatch-mmforc openmatch-mmlogicapi openmatch-evaluator-simple openmatch-mmf-cs-mmlogic-simple openmatch-mmf-go-mmlogic-simple openmatch-mmf-go-grpc-serving-simple openmatch-mmf-py3-mmlogic-simple openmatch-backendclient openmatch-clientloadgen openmatch-frontendclient"
for name in $IMAGE_NAMES
do
source_image=gcr.io/$SOURCE_PROJECT_ID/$name:$SOURCE_VERSION
dest_image=gcr.io/$DEST_PROJECT_ID/$name:$DEST_VERSION
dest_image_latest=gcr.io/$DEST_PROJECT_ID/$name:latest
docker pull $source_image
docker tag $source_image $dest_image
docker tag $source_image $dest_image_latest
docker push $dest_image
docker push $dest_image_latest
done

@ -0,0 +1,82 @@
# Release {version}
<!--
This is the release issue template. Make a copy of the markdown in this page
and copy it into a release issue. Fill in relevent values, found inside {}
{version} should be replaced with the version ie: 0.5.0.
There are 3 types of releases:
* Release Candidates - 1.0.0-rc1
* Full Releases - 1.2.0
* Hot Fixes - 1.0.1
# Release Candidate and Full Release Process
1. Create a Release Issue from the [release issue template](./release_issue.md).
1. Label the issue `kind/release`, and attach it to the milestone that it matches.
1. Complete all items in the release issue checklist.
1. Close the release issue.
# Hot Fix Process
1. Hotfixes will occur as needed, to be determined by those will commit access on the repository.
1. Create a Release Issue from the [release issue template](./release_issue.md).
1. Label the issue `kind/release`, and attach it to the next upcoming milestone.
1. Complete all items in the release issue checklist.
1. Close the release issue.
!-->
Complete Milestone
------------------
- [ ] Create the next version milestone, use [semantic versioning](https://semver.org/) when naming it to be consistent with the [Go community](https://blog.golang.org/versioning-proposal).
- [ ] Visit the [milestone](https://github.com/GoogleCloudPlatform/open-match/milestone).
- [ ] Open a document for a draft [release notes](release.md).
- [ ] Add the milestone tag to all PRs and issues that were merged since the last milestone. Look at the [releases page](https://github.com/GoogleCloudPlatform/open-match/releases) and look for the "X commits to master since this release" for the diff. The link resolves to, https://github.com/GoogleCloudPlatform/open-match/compare/v{version}...master.
- [ ] Review all [milestone-less closed issues](https://github.com/GoogleCloudPlatform/open-match/issues?q=is%3Aissue+is%3Aclosed+no%3Amilestone) and assign the appropriate milestone.
- [ ] Review all [issues in milestone](https://github.com/GoogleCloudPlatform/open-match/milestones) for proper [labels](https://github.com/GoogleCloudPlatform/open-match/labels) (ex: area/build).
- [ ] Review all [milestone-less closed PRs](https://github.com/GoogleCloudPlatform/open-match/pulls?q=is%3Apr+is%3Aclosed+no%3Amilestone) and assign the appropriate milestone.
- [ ] Review all [PRs in milestone](https://github.com/GoogleCloudPlatform/open-match/milestones) for proper [labels](https://github.com/GoogleCloudPlatform/open-match/labels) (ex: area/build).
- [ ] View all open entries in milestone and move them to a future milestone if they aren't getting closed in time. https://github.com/GoogleCloudPlatform/open-match/milestones/v{version}
- [ ] Review all closed PRs against the milestone. Put the user visible changes into the release notes using the suggested format. https://github.com/GoogleCloudPlatform/open-match/pulls?q=is%3Apr+is%3Aclosed+milestone%3Av{version}
- [ ] Review all closed issues against the milestone. Put the user visible changes into the release notes using the suggested format. https://github.com/GoogleCloudPlatform/open-match/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aclosed+milestone%3Av{version}
- [ ] Verify the [milestone](https://github.com/GoogleCloudPlatform/open-match/milestones) is effectively 100% at this point with the exception of the release issue itself.
TODO: Add details for appropriate tagging for issues.
Build Artifacts
---------------
- [ ] Create a PR to bump the version.
- [ ] Open the [`Makefile`](makefile-version) and change BASE_VERSION value. Release candidates use the -rc# suffix.
- [ ] Open the [`install/helm/open-match/Chart.yaml`](om-chart-yaml-version) and [`install/helm/open-match-example/Chart.yaml`](om-example-chart-yaml-version) and change the `appVersion` and `version` entries.
- [ ] Open the [`install/helm/open-match/values.yaml`](om-values-yaml-version) and [`install/helm/open-match-example/values.yaml`](om-example-values-yaml-version) and change the `tag` entries.
- [ ] Open the [`site/config.toml`] and change the `release_branch` and `release_version` entries.
- [ ] Open the [`README.md`](readme-deploy) update the version references.
- [ ] Run `make clean release`
- [ ] There might be additional references to the old version but be careful not to change it for places that have it for historical purposes.
- [ ] Submit the pull request.
- [ ] Take note of the git hash in master, `git checkout master && git pull master && git rev-parse HEAD`
- [ ] Go to [Cloud Build](https://pantheon.corp.google.com/cloud-build/triggers?project=open-match-build), under Post Submit click "Run Trigger".
- [ ] Go to the History section and find the "Post Submit" build that's running. Wait for it to go Green. If it's red fix error repeat this section. Take note of version tag for next step.
- [ ] Run `./docs/governance/templates/release.sh {source version tag} {version}` to copy the images to open-match-public-images.
- [ ] Create a *draft* release with the [release template][release-template]
- [ ] Make a `tag` with the release version. The tag must be v{version}. Example: v0.5.0. Append -rc# for release candidates. Example: v0.5.0-rc1.
- [ ] Copy the files from `build/release/` generated from `make release` from earlier as release artifacts.
- [ ] Run `make delete-gke-cluster create-gke-cluster push-helm sleep-10 install-chart install-example-chart` and verify that the pods are all healthy.
- [ ] Run `make delete-gke-cluster create-gke-cluster` and run through the instructions under the [README](readme-deploy), verify the pods are healthy. You'll need to adjust the path to the `install/yaml/install.yaml` and `install/yaml/install-example.yaml` in your local clone since you haven't published them yet.
- [ ] Publish the [Release](om-release) in Github.
Announce
--------
- [ ] Send an email to the [mailing list](mailing-list-post) with the release details (copy-paste the release blog post)
- [ ] Send a chat on the [Slack channel](om-slack). "Open Match {version} has been released! Check it out at {release url}."
[om-slack]: https://open-match.slack.com/
[mailing-list-post]: https://groups.google.com/forum/#!newtopic/open-match-discuss
[release-template]: https://github.com/GoogleCloudPlatform/open-match/blob/master/docs/governance/templates/release.md
[makefile-version]: https://github.com/GoogleCloudPlatform/open-match/blob/master/Makefile#L53
[om-example-chart-yaml-version]: https://github.com/GoogleCloudPlatform/open-match/blob/master/install/helm/open-match/Chart.yaml#L16
[om-example-values-yaml-version]: https://github.com/GoogleCloudPlatform/open-match/blob/master/install/helm/open-match/values.yaml#L16
[om-example-chart-yaml-version]: https://github.com/GoogleCloudPlatform/open-match/blob/master/install/helm/open-match-example/Chart.yaml#L16
[om-example-values-yaml-version]: https://github.com/GoogleCloudPlatform/open-match/blob/master/install/helm/open-match-example/values.yaml#L16
[om-release]: https://github.com/GoogleCloudPlatform/open-match/releases/new
[readme-deploy]: https://github.com/GoogleCloudPlatform/open-match/blob/master/README.md#deploy-to-kubernetes

17
docs/integrations.md Normal file

@ -0,0 +1,17 @@
## Open Source Software integrations
### Structured logging
Logging for Open Match uses the [Golang logrus module](https://github.com/sirupsen/logrus) to provide structured logs. Logs are output to `stdout` in each component, as expected by Docker and Kubernetes. Level and format are configurable via config/matchmaker_config.json. If you have a specific log aggregator as your final destination, we recommend you have a look at the logrus documentation as there is probably a log formatter that plays nicely with your stack.
### Instrumentation for metrics
Open Match uses [OpenCensus](https://opencensus.io/) for metrics instrumentation. The [gRPC](https://grpc.io/) integrations are built-in, and Golang redigo module integrations are incoming, but [haven't been merged into the official repo](https://github.com/opencensus-integrations/redigo/pull/1). All of the core components expose HTTP `/metrics` endpoints on the port defined in `config/matchmaker_config.json` (default: 9555) for Prometheus to scrape. If you would like to export to a different metrics aggregation platform, we suggest you have a look at the OpenCensus documentation &mdash; there may be one written for you already, and switching to it may be as simple as changing a few lines of code.
**Note:** A standard for instrumentation of MMFs is planned.
### Redis setup
By default, Open Match expects you to run Redis *somewhere*. Connection information can be put in the config file (`matchmaker_config.json`) for any Redis instance reachable from the [Kubernetes namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/). By default, Open Match sensibly runs in the Kubernetes `default` namespace. In most instances, we expect users will run a copy of Redis in a pod in Kubernetes, with a service pointing to it.
* HA configurations for Redis aren't implemented by the provided Kubernetes resource definition files, but Open Match expects the Redis service to be named `redis`, which provides an easier path to multi-instance deployments.

@ -1 +1,33 @@
During alpha, please do not use Open Match as-is in production. To develop against it, please see the [development guide](development.md).
# "Productionizing" a deployment
Here are some steps that should be taken to productionize your Open Match deployment before exposing it to live public traffic. Some of these overlap with best practices for [productionizing Kubernetes](https://cloud.google.com/blog/products/gcp/exploring-container-security-running-a-tight-ship-with-kubernetes-engine-1-10) or cloud infrastructure more generally. We will work to make as many of these into the default deployment strategy for Open Match as possible, going forward.
**This is not an exhaustive list and addressing the items in this document alone shouldn't be considered sufficient. Every game is different and will have different production needs.**
## Kubernetes
All the usual guidance around hardening and securing Kubernetes are applicable to running Open Match. [Here is a guide around security for Google Kubernetes Enginge on GCP](https://cloud.google.com/blog/products/gcp/exploring-container-security-running-a-tight-ship-with-kubernetes-engine-1-10), and a number of other guides are available from reputable sources on the internet.
### Minimum permissions on Kubernetes
* The components of Open Match should be run in a separate Kubernetes namespace if you're also using the cluster for other services. As of 0.3.0 they run in the 'default' namespace if you follow the development guide.
* Note that the default MMForc process has cluster management permissions by default. Before moving to production, you should create a role with only access to create kubernetes jobs and configure the MMForc to use it.
### Kubernetes Jobs (MMFOrc)
The 0.3.0 MMFOrc component runs your MMFs as Kubernetes Jobs. You should periodically delete these jobs to keep the cluster running smoothly. How often you need to delete them is dependant on how many you are running. There are a number of open source solutions to do this for you. ***Note that once you delete the job, you won't have access to that job's logs anymore unless you're sending your logs from kubernetes to a log aggregator like Google Stackdriver. This can make it a challenge to troubleshoot issues***
### CPU and Memory limits
For any production Kubernetes deployment, it is good practice to profile your processes and select [resource limits and requests](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) according to your results. For example, you'll likely want to set adequate resource requests based on your expected player base and some load testing for the Redis state storage pods. This will help Kubernetes avoid scheduling other intensive processes on the same underlying node and keep you from running into resource contention issues. Another example might be an MMF with a particularly large memory or CPU footprint - maybe you have one that searches a lot of players for a potential match. This would be a good candidate for resource limits and requests in Kubernetes to both ensure it gets the CPU and RAM it needs to complete quickly, and to make sure it's not scheduled alongside another intensive Kubernetes pod.
### State storage
The default state storage for Open Match is a _single instance_ of Redis. Although it _is_ possible to go to production with this as the solution if you're willing to accept the potential downsides, for most deployments, a HA Redis configuration would better fit your needs. An example YAML file for creating a [self-healing HA Redis deployment on Kubernetes](../install/yaml/01-redis-failover.yaml) is available. Regardless of which configuation you use, it is probably a good idea to put some [resource requests](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) in your Kubernetes resource definition as mentioned above.
You can find more discussion in the [state storage readme doc](../internal/statestorage/redis/README.md).
## Open Match config
Debug logging and the extra debug code paths should be disabled in the `config/matchmaker_config.json` file (as of the time of this writing, 0.3.0).
## Public APIs for Open Match
In many cases, you may choose to configure your game clients to connect to the Open Match Frontend API, and in a few select cases (such as using it for P2P non-dedicated game server hosting), the game client may also need to connect to the Backend API. In these cases, it is important to secure the API endpoints against common attacks, such as DDoS or malformed packet floods.
* Using a cloud provider's Load Balancer in front of the Kubernetes Service is a common approach to enable vendor-specific DDoS protections. Check the documentation for your cloud vendor's Load Balancer for more details ([GCP's DDoS protection](https://cloud.google.com/armor/)).
* Using an API framework can be used to limit endpoint access to only game clients you have authenticated using your platform's authentication service. This may be accomplished with simple authentication tokens or a more complex scheme depending on your needs.
## Testing
(as of 0.3.0) The provided test programs are just for validating that Open Match is operating correctly; they are command-line applications designed to be run from within the same cluster as Open Match and are therefore not a suitable test harness for doing production testing to make sure your matchmaker is ready to handle your live game. Instead, it is recommended that you integrate Open Match into your game client and test it using the actual game flow players will use if at all possible.
### Load testing
Ideally, you would already be making 'headless' game clients for automated qa and load testing of your game servers; it is recommended that you also code these testing clients to be able to act as a mock player connecting to Open Match. Load testing platform services is a huge topic and should reflect your actual game access patterns as closely as possible, which will be very game dependant.
**Note: It is never a good idea to do load testing against a cloud vendor without informing them first!**

15
docs/references.md Normal file

@ -0,0 +1,15 @@
### Guides
* [Production guide](./docs/production.md) Lots of best practices to be written here before 1.0 release, right now it's a scattered collection of notes. **WIP**
* [Development guide](./docs/development.md)
## This all sounds great, but can you explain Docker and/or Kubernetes to me?
### Docker
- [Docker's official "Getting Started" guide](https://docs.docker.com/get-started/)
- [Katacoda's free, interactive Docker course](https://www.katacoda.com/courses/docker)
### Kubernetes
- [You should totally read this comic, and interactive tutorial](https://cloud.google.com/kubernetes-engine/kubernetes-comic/)
- [Katacoda's free, interactive Kubernetes course](https://www.katacoda.com/courses/kubernetes)

60
docs/roadmap.md Normal file

@ -0,0 +1,60 @@
# Roadmap. [Subject to change]
Releases are scheduled for every 6 weeks. **Every release is a stable, long-term-support version**. Even for alpha releases, best-effort support is available. With a little work and input from an experienced live services developer, you can go to production with any version on the [releases page](https://github.com/GoogleCloudPlatform/open-match/releases).
Our current thinking is to wait to take Open Match out of alpha/beta (and label it 1.0) until it can be used out-of-the-box, standalone, for developers that dont have any existing platform services. Which is to say, the majority of **established game developers likely won't have any reason to wait for the 1.0 release if Open Match already handles your needs**. If you already have live platform services that you plan to integrate Open Match with (player authentication, a group invite system, dedicated game servers, metrics collection, logging aggregation, etc), then a lot of the features planned between 0.4.0 and 1.0 likely aren't of much interest to you anyway.
## Upcoming releases
* **0.4.0** &mdash; Agones Integration & MMF on [Knative](https://cloud.google.com/knative/)
MMF instrumentation
Match object expiration / lazy deletion
API autoscaling by default
API changes after this will likely be additions or very minor
* **0.5.0** &mdash; Tracing, Metrics, and KPI Dashboard
* **0.6.0** &mdash; Load testing suite
* **1.0.0** &mdash; API Formally Stable. Breaking API changes will require a new major version number.
* **1.1.0** &mdash; Canonical MMFs
## Philosophy
* The next version (0.4.0) will focus on making MMFs run on serverless platforms - specifically Knative. This will just be first steps, as Knative is still pretty early. We want to get a proof of concept working so we can roadmap out the future "MMF on Knative" experience. Our intention is to keep MMFs as compatible as possible with the current Kubernetes job-based way of doing them. Our hope is that by the time Knative is mature, well be able to provide a [Knative build](https://github.com/Knative/build) pipeline that will take existing MMFs and build them as Knative functions. In the meantime, well map out a relatively painless (but not yet fully automated) way to make an existing MMF into a Kubernetes Deployment that looks as similar to what [Knative serving](https://github.com/knative/serving) is shaping up to be, in an effort to make the eventual switchover painless. Basically all of this is just _optimizing MMFs to make them spin up faster and take less resources_, **we're not planning to change what MMFs do or the interfaces they need to fulfill**. Existing MMFs will continue to run as-is, and in the future moving them to Knative should be both **optional** and **largely automated**.
* 0.4.0 represents the natural stopping point for adding new functionality until we have more community uptake and direction. We don't anticipate many API changes in 0.4.0 and beyond. Maybe new API calls for new functionality, but we're unlikely to see big shifts in existing calls through 1.0 and its point releases. We'll issue a new major release version if we decide we need those changes.
* The 0.5.0 version and beyond will be focused on operationalizing the out-of-the-box experience. Metrics and analytics and a default dashboard, additional tooling, and a load testing suite are all planned. We want it to be easy for operators to see KPI and know what's going on with Open Match.
# Planned improvements
See the [provisional roadmap](docs/roadmap.md) for more information on upcoming releases.
## Documentation
- [ ] “Writing your first matchmaker” getting started guide will be included in an upcoming version.
- [ ] Documentation for using the example customizable components and the `backendstub` and `frontendstub` applications to do an end-to-end (e2e) test will be written. This all works now, but needs to be written up.
- [ ] Documentation on release process and release calendar.
## State storage
- [X] All state storage operations should be isolated from core components into the `statestorage/` modules. This is necessary precursor work to enabling Open Match state storage to use software other than Redis.
- [X] [The Redis deployment should have an example HA configuration](https://github.com/GoogleCloudPlatform/open-match/issues/41)
- [X] Redis watch should be unified to watch a hash and stream updates. The code for this is written and validated but not committed yet.
- [ ] We don't want to support two redis watcher code paths, but we will until golang protobuf reflection is a bit more usable. [Design doc](https://docs.google.com/document/d/19kfhro7-CnBdFqFk7l4_HmwaH2JT_Rhw5-2FLWLEGGk/edit#heading=h.q3iwtwhfujjx), [github issue](https://github.com/golang/protobuf/issues/364)
- [X] Player/Group records generated when a client enters the matchmaking pool need to be removed after a certain amount of time with no activity. When using Redis, this will be implemented as a expiration on the player record.
## Instrumentation / Metrics / Analytics
- [ ] Instrumentation of MMFs is in the planning stages. Since MMFs are by design meant to be completely customizable (to the point of allowing any process that can be packaged in a Docker container), metrics/stats will need to have an expected format and formalized outgoing pathway. Currently the thought is that it might be that the metrics should be written to a particular key in statestorage in a format compatible with opencensus, and will be collected, aggreggated, and exported to Prometheus using another process.
- [ ] [OpenCensus tracing](https://opencensus.io/core-concepts/tracing/) will be implemented in an upcoming version. This is likely going to require knative.
- [X] Read logrus logging configuration from matchmaker_config.json.
## Security
- [ ] The Kubernetes service account used by the MMFOrc should be updated to have min required permissions. [Issue 52](issues/52)
## Kubernetes
- [ ] Autoscaling isn't turned on for the Frontend or Backend API Kubernetes deployments by default.
- [X] A [Helm](https://helm.sh/) chart to stand up Open Match may be provided in an upcoming version. For now just use the [installation YAMLs](./install/yaml).
- [ ] A knative-based implementation of MMFs is in the planning stages.
## CI / CD / Build
- [X] We plan to host 'official' docker images for all release versions of the core components in publicly available docker registries soon. This is tracked in [Issue #45](issues/45) and is blocked by [Issue 42](issues/42).
- [X] CI/CD for this repo and the associated status tags are planned.
- [ ] Golang unit tests will be shipped in an upcoming version.
- [ ] A full load-testing and e2e testing suite will be included in an upcoming version.
## Will not Implement
- [X] Defining multiple images inside a profile for the purposes of experimentation adds another layer of complexity into profiles that can instead be handled outside of open match with custom match functions in collaboration with a director (thing that calls backend to schedule matchmaking)
### Special Thanks
- Thanks to https://jbt.github.io/markdown-editor/ for help in marking this document down.

30
examples/backendclient/Dockerfile Executable file → Normal file

@ -1,8 +1,24 @@
#FROM golang:1.10.3 as builder
FROM gcr.io/matchmaker-dev-201405/openmatch-devbase as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/examples/backendclient
COPY ./ ./
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o backendclient .
# Copyright 2019 Google LLC
#
# 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.
CMD ["./backendclient"]
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/examples/backendclient/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/examples/backendclient/backendclient .
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/examples/backendclient/profiles profiles
ENTRYPOINT ["/backendclient"]

@ -1,11 +0,0 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [ 'pull', 'gcr.io/$PROJECT_ID/openmatch-devbase' ]
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'--tag=gcr.io/$PROJECT_ID/openmatch-backendclient:dev',
'--cache-from=gcr.io/$PROJECT_ID/openmatch-devbase:latest',
'.'
]
images: ['gcr.io/$PROJECT_ID/openmatch-backendclient:dev']

@ -4,7 +4,6 @@ assumes that the backend api is up and can be accessed through a k8s service
named om-backendapi
Copyright 2018 Google LLC
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
@ -25,16 +24,26 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"flag"
"io"
"io/ioutil"
"log"
"net"
"os"
backend "github.com/GoogleCloudPlatform/open-match/internal/pb"
"github.com/GoogleCloudPlatform/open-match/internal/pb"
"github.com/gobs/pretty"
"github.com/tidwall/gjson"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
var (
filename = flag.String("file", "profiles/testprofile.json", "JSON file from which to read match properties")
beCall = flag.String("call", "ListMatches", "Open Match backend match request gRPC call to test")
server = flag.String("backend", "om-backendapi:50505", "Hostname and IP of the Open Match backend")
assignment = flag.String("assignment", "example.server.dgs:12345", "Assignment to send to matched players")
delAssignments = flag.Bool("rm", false, "Delete assignments. Leave off to be able to manually validate assignments in state storage")
verbose = flag.Bool("verbose", false, "Print out as much as possible")
)
func bytesToString(data []byte) string {
@ -42,23 +51,31 @@ func bytesToString(data []byte) string {
}
func ppJSON(s string) {
buf := new(bytes.Buffer)
json.Indent(buf, []byte(s), "", " ")
log.Println(buf)
if *verbose {
buf := new(bytes.Buffer)
json.Indent(buf, []byte(s), "", " ")
log.Println(buf)
}
return
}
func main() {
flag.Parse()
log.Print("Parsing flags:")
log.Printf(" [flags] Reading properties from file at %v", *filename)
log.Printf(" [flags] Using OM Backend address %v", *server)
log.Printf(" [flags] Using OM Backend %v call", *beCall)
log.Printf(" [flags] Assigning players to %v", *assignment)
log.Printf(" [flags] Deleting assignments? %v", *delAssignments)
if !(*beCall == "CreateMatch" || *beCall == "ListMatches") {
log.Printf(" [flags] Unknown OM Backend call %v! Exiting...", *beCall)
return
}
// Read the profile
filename := "profiles/testprofile.json"
if len(os.Args) > 1 {
filename = os.Args[1]
}
log.Println("Reading profile from ", filename)
jsonFile, err := os.Open(filename)
jsonFile, err := os.Open(*filename)
if err != nil {
panic("Failed to open file specified at command line. Did you forget to specify one?")
log.Fatalf("Failed to open file %v", *filename)
}
defer jsonFile.Close()
@ -70,111 +87,137 @@ func main() {
}
jsonProfile := buffer.String()
pbProfile := &backend.MatchObject{}
/*
err = jsonpb.UnmarshalString(jsonProfile, pbProfile)
if err != nil {
log.Println(err)
}
*/
pbProfile := &pb.MatchObject{}
pbProfile.Properties = jsonProfile
log.Println("Requesting matches that fit profile:")
ppJSON(jsonProfile)
//jsonProfile := bytesToString(jsonData)
// Connect gRPC client
ip, err := net.LookupHost("om-backendapi")
if err != nil {
panic(err)
}
conn, err := grpc.Dial(ip[0]+":50505", grpc.WithInsecure())
conn, err := grpc.Dial(*server, grpc.WithInsecure())
if err != nil {
log.Fatalf("failed to connect: %s", err.Error())
}
client := backend.NewBackendClient(conn)
log.Println("API client connected to", ip[0]+":50505")
profileName := "test-dm-usc1f"
_ = profileName
client := pb.NewBackendClient(conn)
log.Println("Backend client connected to", *server)
var profileName string
if gjson.Get(jsonProfile, "name").Exists() {
profileName = gjson.Get(jsonProfile, "name").String()
} else {
profileName = "testprofilename"
log.Println("JSON Profile does not contain a name; using ", profileName)
}
pbProfile.Id = profileName
pbProfile.Properties = jsonProfile
mmfcfg := &pb.MmfConfig{Name: "profileName"}
mmfcfg.Type = pb.MmfConfig_GRPC
mmfcfg.Host = gjson.Get(jsonProfile, "hostname").String()
mmfcfg.Port = int32(gjson.Get(jsonProfile, "port").Int())
req := &pb.CreateMatchRequest{
Match: pbProfile,
Mmfcfg: mmfcfg,
}
log.Println("Backend Request:")
ppJSON(jsonProfile)
pretty.PrettyPrint(mmfcfg)
log.Printf("Establishing HTTPv2 stream...")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
//match, err := client.CreateMatch(ctx, pbProfile)
for {
log.Println("Attempting to send ListMatches call")
stream, err := client.ListMatches(ctx, pbProfile)
matchChan := make(chan *pb.MatchObject)
doneChan := make(chan bool)
go func() {
// Watch for results and print as they come in.
log.Println("Watching for match results...")
for {
select {
case match := <-matchChan:
if match.Error == "insufficient players" {
log.Println("Waiting for a larger player pool...")
}
// Validate JSON before trying to parse it
if !gjson.Valid(string(match.Properties)) {
log.Println(errors.New("invalid json"))
}
log.Println("Received match:")
pretty.PrettyPrint(match)
// Assign players in this match to our server
log.Println("Assigning players to DGS at", *assignment)
assign := &pb.Assignments{Rosters: match.Rosters, Assignment: *assignment}
log.Printf("Waiting for matches...")
_, err = client.CreateAssignments(context.Background(), &pb.CreateAssignmentsRequest{
Assignment: assign,
})
if err != nil {
log.Println(err)
}
log.Println("Success!")
if *delAssignments {
log.Println("deleting assignments")
for _, a := range assign.Rosters {
_, err = client.DeleteAssignments(context.Background(), &pb.DeleteAssignmentsRequest{Roster: a})
if err != nil {
log.Println(err)
}
log.Println("Success Deleting Assignments!")
}
} else {
log.Println("Not deleting assignments [demo mode].")
}
}
if *beCall == "CreateMatch" {
// Got a result; done here.
log.Println("Got single result from CreateMatch, exiting...")
doneChan <- true
return
}
}
}()
// Make the requested backend call: CreateMatch calls once, ListMatches continually calls.
log.Printf("Attempting %v() call", *beCall)
switch *beCall {
case "CreateMatch":
resp, err := client.CreateMatch(ctx, req)
if err != nil {
panic(err)
}
log.Printf("CreateMatch returned; processing match")
matchChan <- resp.Match
<-doneChan
case "ListMatches":
stream, err := client.ListMatches(ctx, &pb.ListMatchesRequest{
Mmfcfg: req.Mmfcfg,
Match: req.Match,
})
if err != nil {
log.Fatalf("Attempting to open stream for ListMatches(_) = _, %v", err)
}
log.Printf("Waiting for matches...")
//for i := 0; i < 2; i++ {
for {
match, err := stream.Recv()
log.Printf("Waiting for matches...")
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("Error reading stream for ListMatches(_) = _, %v", err)
break
}
if match.Properties == "{error: insufficient_players}" {
log.Println("Waiting for a larger player pool...")
break
}
// Validate JSON before trying to parse it
if !gjson.Valid(string(match.Properties)) {
log.Println(errors.New("invalid json"))
}
log.Println("Received match:")
ppJSON(match.Properties)
fmt.Println(match)
/*
// Get players from the json properties.roster field
log.Println("Gathering roster from received match...")
players := make([]string, 0)
result := gjson.Get(match.Properties, "properties.roster")
result.ForEach(func(teamName, teamRoster gjson.Result) bool {
teamRoster.ForEach(func(_, player gjson.Result) bool {
players = append(players, player.String())
return true // keep iterating
})
return true // keep iterating
})
//log.Printf("players = %+v\n", players)
// Assign players in this match to our server
log.Println("Assigning players to DGS at example.com:12345")
playerstr := strings.Join(players, " ")
roster := &backend.Roster{PlayerIds: playerstr}
ci := &backend.ConnectionInfo{ConnectionString: "example.com:12345"}
assign := &backend.Assignments{Roster: roster, ConnectionInfo: ci}
_, err = client.CreateAssignments(context.Background(), assign)
if err != nil {
panic(err)
stat, ok := status.FromError(err)
if ok {
log.Printf("Error reading stream for ListMatches() returned status: %s %s", stat.Code().String(), stat.Message())
} else {
log.Printf("Error reading stream for ListMatches() returned status: %s", err)
}
*/
break
}
matchChan <- resp.Match
}
//log.Println("deleting assignments")
//playerstr = strings.Join(players[0:len(players)/2], " ")
//roster.PlayerIds = playerstr
//_, err = client.DeleteAssignments(context.Background(), roster)
}
}

@ -1,7 +1,8 @@
{
"imagename":"gcr.io/matchmaker-dev-201405/openmatch-mmf:py3",
"name":"testprofilev1",
"id":"testprofile",
"hostname": "om-function",
"port": 50502,
"properties":{
"pools": [
{

@ -0,0 +1,23 @@
# Copyright 2019 Google LLC
#
# 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.
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/examples/evaluators/golang/serving/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/examples/evaluators/golang/serving/serving .
ENTRYPOINT ["/serving"]

@ -0,0 +1,125 @@
/*
This is a sample Evaluator built using the Evaluator Harness. It evaluates
multiple proposals and approves a subset of them. This sample demonstrates
how to build a basic Evaluator using the Evaluator Harness . This example
over-simplifies the actual evaluation decisions and hence should not be
used as is for a real scenario.
Copyright 2018 Google LLC
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
https://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 main
import (
"context"
"fmt"
harness "github.com/GoogleCloudPlatform/open-match/internal/harness/evaluator/golang"
"github.com/GoogleCloudPlatform/open-match/internal/pb"
)
func main() {
// This invoke the harness to set up the Evaluator. The harness abstracts
// fetching proposals ready for evaluation and the process of transforming
// approved proposals into results that Open Match can relay to the caller
// requesting for matches.
harness.RunEvaluator(Evaluate)
}
// Evaluate is where your custom evaluation logic lives.
// Input:
// - proposals : List of all the proposals to be consiered for evaluation. Each proposal will have
// Rosters comprising of the players belonging to that proposal.
// Output:
// - (proposals) : List of approved proposal IDs that can be returned as match results.
func Evaluate(ctx context.Context, proposals []*pb.MatchObject) ([]string, error) {
// Map of approved and overloaded proposals. Using maps for easier lookup.
approvedProposals := map[string]bool{}
overloadedProposals := map[string]bool{}
// Map of all the players encountered in the proposals. Each entry maps a player id to
// the first match in which the player was encountered.
allPlayers := map[string]string{}
// Iterate over each proposal to either add to approved map or overloaded map.
for _, proposal := range proposals {
proposalID := proposal.Id
approved := true
players := getPlayersInProposal(proposal)
// Iterate over each player in the proposal to check if the player was encountered before.
for _, playerID := range players {
if propID, found := allPlayers[playerID]; found {
// Player was encountered in an earlier proposal. Mark the current proposal as overloaded (not approved).
// Also, the first proposal where the player was encountered may have been marked approved. Remove that proposal
// approved proposals and add to overloaded proposals since we encountered its player in current proposal too.
approved = false
delete(approvedProposals, propID)
overloadedProposals[propID] = true
} else {
// Player encountered for the first time, add to all players map with the current proposal.
allPlayers[playerID] = proposalID
}
if approved {
approvedProposals[proposalID] = true
} else {
overloadedProposals[proposalID] = true
}
}
}
// Convert the maps to lists of overloaded, approved proposals.
overloadedList := []string{}
approvedList := []string{}
for k := range overloadedProposals {
overloadedList = append(overloadedList, k)
}
for k := range approvedProposals {
approvedList = append(approvedList, k)
}
// Select proposals to approve from the overloaded proposals list.
chosen, err := chooseProposals(overloadedList)
if err != nil {
return nil, fmt.Errorf("Failed to select approved list from overloaded proposals, %v", err)
}
// Add the chosen proposals to the approved list.
approvedList = append(approvedList, chosen...)
return approvedList, nil
}
// chooseProposals should look through all overloaded proposals (that is, have a player that is also
// in another proposed match) and choose the proposals to approve. This is where the core evaluation
// logic will be added.
func chooseProposals(overloaded []string) ([]string, error) {
// As a basic example, we pick the first overloaded proposal for approval.
approved := []string{}
if len(overloaded) > 0 {
approved = append(approved, overloaded[0])
}
return approved, nil
}
func getPlayersInProposal(proposal *pb.MatchObject) []string {
var players []string
for _, r := range proposal.Rosters {
for _, p := range r.Players {
players = append(players, p.Id)
}
}
return players
}

@ -1,302 +0,0 @@
// The evaluator is, generalized, a weighted graph problem
// https://www.google.co.jp/search?q=computer+science+weighted+graph&oq=computer+science+weighted+graph
// However, it's up to the developer to decide what values in their matchmaking
// decision process are the weights as well as what to prioritize (make as many
// groups as possible is a common goal). The default evaluator makes naive
// decisions under the assumption that most use cases would rather spend their
// time tweaking the profiles sent to matchmaking such that they are less and less
// likely to choose the same players.
/*
Copyright 2018 Google LLC
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
https://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 main
import (
"context"
"fmt"
"log"
"os"
"strings"
"time"
om_messages "github.com/GoogleCloudPlatform/open-match/internal/pb"
"github.com/GoogleCloudPlatform/open-match/internal/statestorage/redis/redispb"
"github.com/gobs/pretty"
"github.com/gomodule/redigo/redis"
"github.com/spf13/viper"
)
func main() {
//Init Logger
lgr := log.New(os.Stdout, "MMFEval: ", log.LstdFlags)
lgr.Println("Initializing example MMF proposal evaluator")
// Read config
lgr.Println("Initializing config...")
cfg, err := readConfig("matchmaker_config", map[string]interface{}{
"REDIS_SENTINEL_SERVICE_HOST": "redis-sentinel",
"REDIS_SENTINEL_SERVICE_PORT": "6379",
"auth": map[string]string{
// Read from k8s secret eventually
// Probably doesn't need a map, just here for reference
"password": "12fa",
},
})
if err != nil {
panic(nil)
}
// Connect to redis
// As per https://www.iana.org/assignments/uri-schemes/prov/redis
// redis://user:secret@localhost:6379/0?foo=bar&qux=baz // redis pool docs: https://godoc.org/github.com/gomodule/redigo/redis#Pool
redisURL := "redis://" + cfg.GetString("REDIS_SENTINEL_SERVICE_HOST") + ":" + cfg.GetString("REDIS_SENTINEL_SERVICE_PORT")
lgr.Println("Connecting to redis at", redisURL)
pool := redis.Pool{
MaxIdle: 3,
MaxActive: 0,
IdleTimeout: 60 * time.Second,
Dial: func() (redis.Conn, error) { return redis.DialURL(redisURL) },
}
redisConn := pool.Get()
defer redisConn.Close()
// TODO: write some code to allow the context to be safely cancelled
/*
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
*/
start := time.Now()
proposedMatchIds, overloadedPlayers, overloadedMatches, approvedMatches, err := stub(cfg, &pool)
overloadedPlayerList, overloadedMatchList, approvedMatchList := generateLists(overloadedPlayers, overloadedMatches, approvedMatches)
fmt.Println("overloadedPlayers")
pretty.PrettyPrint(overloadedPlayers)
fmt.Println("overloadePlayerList")
pretty.PrettyPrint(overloadedPlayerList)
fmt.Println("overloadedMatchList")
pretty.PrettyPrint(overloadedMatchList)
fmt.Println("approvedMatchList")
pretty.PrettyPrint(approvedMatchList)
approved, rejected, err := chooseMatches(overloadedMatchList)
approvedMatchList = append(approvedMatchList, approved...)
// run redis commands to approve matches
for _, proposalIndex := range approvedMatchList {
// The match object was already written by the MMF, just change the
// name to what the Backend API (apisrv.go) is looking for.
proposedID := proposedMatchIds[proposalIndex]
// Incoming proposal keys look like this:
// proposal.1542600048.80e43fa085844eebbf53fc736150ef96.testprofile
// format:
// "proposal".timestamp.unique_matchobject_id.profile_name
values := strings.Split(proposedID, ".")
moID, proID := values[2], values[3]
backendID := moID + "." + proID
fmt.Printf("approving proposal #%+v:%+v\n", proposalIndex, moID)
fmt.Println("RENAME", proposedID, backendID)
_, err = redisConn.Do("RENAME", proposedID, backendID)
if err != nil {
// RENAME only fails if the source key doesn't exist
fmt.Printf("err = %+v\n", err)
}
}
//TODO: Need to requeue for another job run here.
for _, proposalIndex := range rejected {
fmt.Println("rejecting ", proposalIndex)
proposedID := proposedMatchIds[proposalIndex]
fmt.Printf("proposedID = %+v\n", proposedID)
values := strings.Split(proposedID, ".")
fmt.Printf("values = %+v\n", values)
timestamp, moID, proID := values[0], values[1], values[2]
fmt.Printf("timestamp = %+v\n", timestamp)
fmt.Printf("moID = %+v\n", moID)
fmt.Printf("proID = %+v\n", proID)
}
lgr.Printf("0 Finished in %v seconds.", time.Since(start).Seconds())
}
// chooseMatches looks through all match proposals that ard overloaded (that
// is, have a player that is also in another proposed match) and chooses those
// to approve and those to reject.
// TODO: this needs a complete overhaul in a 'real' graph search
func chooseMatches(overloaded []int) ([]int, []int, error) {
// Super naive - take one overloaded match and approved it, reject all others.
fmt.Printf("overloaded = %+v\n", overloaded)
fmt.Printf("len(overloaded) = %+v\n", len(overloaded))
if len(overloaded) > 0 {
fmt.Printf("overloaded[0:2] = %+v\n", overloaded[0:0])
fmt.Printf("overloaded[1:] = %+v\n", overloaded[1:])
return overloaded[0:1], overloaded[1:], nil
}
return []int{}, overloaded, nil
}
func readConfig(filename string, defaults map[string]interface{}) (*viper.Viper, error) {
/*
Examples of redis-related env vars as written by k8s
REDIS_SENTINEL_PORT_6379_TCP=tcp://10.55.253.195:6379
REDIS_SENTINEL_PORT=tcp://10.55.253.195:6379
REDIS_SENTINEL_PORT_6379_TCP_ADDR=10.55.253.195
REDIS_SENTINEL_SERVICE_PORT=6379
REDIS_SENTINEL_PORT_6379_TCP_PORT=6379
REDIS_SENTINEL_PORT_6379_TCP_PROTO=tcp
REDIS_SENTINEL_SERVICE_HOST=10.55.253.195
*/
v := viper.New()
for key, value := range defaults {
v.SetDefault(key, value)
}
v.SetConfigName(filename)
v.SetConfigType("json")
v.AddConfigPath(".")
v.AutomaticEnv()
// Optional read from config if it exists
err := v.ReadInConfig()
if err != nil {
//lgr.Printf("error when reading config: %v\n", err)
//lgr.Println("continuing...")
err = nil
}
return v, err
}
func stub(cfg *viper.Viper, pool *redis.Pool) ([]string, map[string][]int, map[int][]int, map[int]bool, error) {
//Init Logger
lgr := log.New(os.Stdout, "MMFEvalStub: ", log.LstdFlags)
lgr.Println("Initializing example MMF proposal evaluator")
// Get redis conneciton
redisConn := pool.Get()
defer redisConn.Close()
// Put some config vars into other vars for readability
proposalq := cfg.GetString("queues.proposals.name")
lgr.Println("SCARD", proposalq)
numProposals, err := redis.Int(redisConn.Do("SCARD", proposalq))
lgr.Println("SPOP", proposalq, numProposals)
proposals, err := redis.Strings(redisConn.Do("SPOP", proposalq, numProposals))
if err != nil {
lgr.Println(err)
}
fmt.Printf("proposals = %+v\n", proposals)
// This is a far cry from effecient but we expect a pretty small set of players under consideration
// at any given time
// Map that implements a set https://golang.org/doc/effective_go.html#maps
overloadedPlayers := make(map[string][]int)
overloadedMatches := make(map[int][]int)
approvedMatches := make(map[int]bool)
allPlayers := make(map[string]int)
// Loop through each proposal, and look for 'overloaded' players (players in multiple proposals)
for index, propKey := range proposals {
approvedMatches[index] = true // This proposal is approved until proven otherwise
playerList, err := getProposedPlayers(pool, propKey)
if err != nil {
lgr.Println(err)
}
for _, pID := range playerList {
if allPlayers[pID] != 0 {
// Seen this player at least once before; gather the indicies of all the match
// proposals with this player
overloadedPlayers[pID] = append(overloadedPlayers[pID], index)
overloadedMatches[index] = []int{}
delete(approvedMatches, index)
if len(overloadedPlayers[pID]) == 1 {
adjustedIndex := allPlayers[pID] - 1
overloadedPlayers[pID] = append(overloadedPlayers[pID], adjustedIndex)
overloadedMatches[adjustedIndex] = []int{}
delete(approvedMatches, adjustedIndex)
}
} else {
// First time seeing this player. Track which match proposal had them in case we see
// them again
// Terrible indexing hack: default int value is 0, but so is the
// lowest propsal index. Since we need to use interpret 0 as
// 'unset' in this context, add one to index, and remove it if/when we put this
// player in the overloadedPlayers map.
adjustedIndex := index + 1
allPlayers[pID] = adjustedIndex
}
}
}
return proposals, overloadedPlayers, overloadedMatches, approvedMatches, err
}
// getProposedPlayers is a function that may be moved to an API call in the future.
func getProposedPlayers(pool *redis.Pool, propKey string) ([]string, error) {
// Get the proposal match object from redis
mo := &om_messages.MatchObject{Id: propKey}
err := redispb.UnmarshalFromRedis(context.Background(), pool, mo)
if err != nil {
return nil, err
}
// Loop through all rosters, appending players IDs to a list.
playerList := make([]string, 0)
for _, r := range mo.Rosters {
for _, p := range r.Players {
playerList = append(playerList, p.Id)
}
}
return playerList, err
}
func propToRoster(in []string) []interface{} {
// Convert []string to []interface{} so it can be passed as variadic input
// https://golang.org/doc/faq#convert_slice_of_interface
out := make([]interface{}, len(in))
for i, v := range in {
values := strings.Split(v, ".")
timestamp, moID, proID := values[0], values[1], values[2]
out[i] = "roster." + timestamp + "." + moID + "." + proID
}
return out
}
func generateLists(overloadedPlayers map[string][]int, overloadedMatches map[int][]int, approvedMatches map[int]bool) ([]string, []int, []int) {
// Make a slice of overloaded players from the map.
overloadedPlayerList := make([]string, 0, len(overloadedPlayers))
for k := range overloadedPlayers {
overloadedPlayerList = append(overloadedPlayerList, k)
}
// Make a slice of overloaded matches from the set.
overloadedMatchList := make([]int, 0, len(overloadedMatches))
for k := range overloadedMatches {
overloadedMatchList = append(overloadedMatchList, k)
}
// Make a slice of approved matches from the set.
approvedMatchList := make([]int, 0, len(approvedMatches))
for k := range approvedMatches {
approvedMatchList = append(approvedMatchList, k)
}
return overloadedPlayerList, overloadedMatchList, approvedMatchList
}

@ -1 +0,0 @@
../../../../config/matchmaker_config.json

Some files were not shown because too many files have changed in this diff Show More