diff --git a/docs/gameserverbuild.md b/docs/gameserverbuild.md index 17a56e30..0785716c 100755 --- a/docs/gameserverbuild.md +++ b/docs/gameserverbuild.md @@ -1,39 +1,39 @@ -# GameServerBuild definition - -A GameServerBuild is equivalent to a Build region in MPS. GameServer containers that work in thundernetes should work in a similar way on PlayFab Multiplayer Servers service. - -Here you can see the YAML that can be used to create a GameServerBuild in thundernetes. Fields are similar to the ones used on MPS. - -```yaml -apiVersion: mps.playfab.com/v1alpha1 -kind: GameServerBuild -metadata: - name: gameserverbuild-sample # required, name of the GameServerBuild -spec: - titleID: "1E03" # required, corresponds to the PlayFab TitleID your game server is using. Can be an arbitrary string - buildID: "85ffe8da-c82f-4035-86c5-9d2b5f42d6f5" # required, build ID of your game, must be GUID. Will be used for allocations - standingBy: 2 # required, number of standing by servers to create - max: 4 # reqired, max number of servers to create. Active+StandingBy servers will never be larger than max - crashesToMarkUnhealthy: 5 # optional, default is 5. It is the number of crashes needed to mark the GameServerBuild unhealthy. Once this happens, no other operation will take place - buildMetadata: # optional. Retrievable via GSDK, used to customize your game server - - key: "buildMetadataKey1" - value: "buildMetadataValue1" - - key: "buildMetadataKey1" - value: "buildMetadataValue1" - portsToExpose: # port names that you need to expose for your game server, read more below - - containerName: gameserver-sample # name of the container that you want its port exposed - portName: gameport # name of the port that you want to expose - podSpec: - containers: - - image: youGameServerImage:tag # image of your game server - name: gameserver-sample # name of the container. Must be the same as portsToExpose.containerName - ports: - - containerPort: 7777 # port that you want to expose - name: gameport # name of the port that you want to expose. Must be the same as portsToExpose.portName -``` - -The podSpec contains the definition for a [Kubernetes Pod](https://kubernetes.io/docs/concepts/workloads/pods/). As a result, you should include here whatever is needed for your game server (environment variables, storage, etc). Bear in mind though that not everything will work in MPS though. - -## PortsToExpose - -This is a list of containerName/portName tuples: These are the ports that you want to be exposed in the [Worker Node/VM](https://kubernetes.io/docs/concepts/architecture/nodes/) when the Pod is created. The way this works is that each Pod you create will have >=1 number of containers. There, each container will have its own *Ports* definition. If a port in this definition is included in the *portsToExpose* array, this port will be publicly exposed in the Node/VM. This is accomplished by the creation of a **hostPort** value for each of the container ports you want to expose. The reason we need this is that because i) you may want to use some ports on your Pod containers for other purposed than players connecting to it and ii) a portName must be unique within a container. Ports assigned are in the port range 10000-50000. +# GameServerBuild definition + +A GameServerBuild is equivalent to a Build region in MPS. GameServer containers that work in thundernetes should work in a similar way on PlayFab Multiplayer Servers service. + +Here you can see the YAML that can be used to create a GameServerBuild in thundernetes. Fields are similar to the ones used on MPS. + +```yaml +apiVersion: mps.playfab.com/v1alpha1 +kind: GameServerBuild +metadata: + name: gameserverbuild-sample # required, name of the GameServerBuild +spec: + titleID: "1E03" # required, corresponds to the PlayFab TitleID your game server is using. Can be an arbitrary string + buildID: "85ffe8da-c82f-4035-86c5-9d2b5f42d6f5" # required, build ID of your game, must be GUID. Will be used for allocations + standingBy: 2 # required, number of standing by servers to create + max: 4 # reqired, max number of servers to create. Active+StandingBy servers will never be larger than max + crashesToMarkUnhealthy: 5 # optional, default is 5. It is the number of crashes needed to mark the GameServerBuild unhealthy. Once this happens, no other operation will take place + buildMetadata: # optional. Retrievable via GSDK, used to customize your game server + - key: "buildMetadataKey1" + value: "buildMetadataValue1" + - key: "buildMetadataKey1" + value: "buildMetadataValue1" + portsToExpose: # port names that you need to expose for your game server, read more below + - containerName: gameserver-sample # name of the container that you want its port exposed + portName: gameport # name of the port that you want to expose + podSpec: + containers: + - image: youGameServerImage:tag # image of your game server + name: gameserver-sample # name of the container. Must be the same as portsToExpose.containerName + ports: + - containerPort: 7777 # port that you want to expose + name: gameport # name of the port that you want to expose. Must be the same as portsToExpose.portName +``` + +The podSpec contains the definition for a [Kubernetes Pod](https://kubernetes.io/docs/concepts/workloads/pods/). As a result, you should include here whatever is needed for your game server (environment variables, storage, etc). Bear in mind though that not everything will work in MPS though. + +## PortsToExpose + +This is a list of containerName/portName tuples: These are the ports that you want to be exposed in the [Worker Node/VM](https://kubernetes.io/docs/concepts/architecture/nodes/) when the Pod is created. The way this works is that each Pod you create will have >=1 number of containers. There, each container will have its own *Ports* definition. If a port in this definition is included in the *portsToExpose* array, this port will be publicly exposed in the Node/VM. This is accomplished by the creation of a **hostPort** value for each of the container ports you want to expose. The reason we need this is that because i) you may want to use some ports on your Pod containers for other purposed than players connecting to it and ii) a portName must be unique within a container. Ports assigned are in the port range 10000-50000. diff --git a/initcontainer/go.mod b/initcontainer/go.mod index d843215c..654b7ea0 100644 --- a/initcontainer/go.mod +++ b/initcontainer/go.mod @@ -2,4 +2,7 @@ module github.com/playfab/thundernetes/initcontainer go 1.16 -require github.com/pkg/errors v0.9.1 // indirect +require ( + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect +) diff --git a/initcontainer/go.sum b/initcontainer/go.sum index 7c401c3f..17ab7b38 100644 --- a/initcontainer/go.sum +++ b/initcontainer/go.sum @@ -1,2 +1,9 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/initcontainer/main.go b/initcontainer/main.go index 5de52e00..2247009c 100644 --- a/initcontainer/main.go +++ b/initcontainer/main.go @@ -1,180 +1,198 @@ -package main - -import ( - "encoding/json" - "log" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/pkg/errors" -) - -// GsdkConfig is the configuration for the GSDK -// it will be written to the file that will be read by the GSDK running alongside the GameServer -type GsdkConfig struct { - HeartbeatEndpoint string `json:"heartbeatEndpoint"` - SessionHostId string `json:"sessionHostId"` - VmId string `json:"vmId"` - LogFolder string `json:"logFolder"` - CertificateFolder string `json:"certificateFolder"` - SharedContentFolder string `json:"sharedContentFolder"` - BuildMetadata map[string]string `json:"buildMetadata"` - GamePorts map[string]int `json:"gamePorts"` - PublicIpV4Address string `json:"publicIpV4Address"` - GameServerConnectionInfo GameServerConnectionInfo `json:"gameServerConnectionInfo"` - ServerInstanceNumber int `json:"serverInstanceNumber"` // Not used - FullyQualifiedDomainName string `json:"fullyQualifiedDomainName"` -} - -type GameServerConnectionInfo struct { - PublicIpV4Address string `json:"publicIpV4Address"` - GamePortsConfiguration []GamePort `json:"gamePortsConfiguration"` -} - -type GamePort struct { - Name string `json:"name"` - ServerListeningPort int `json:"serverListeningPort"` - ClientConnectionPort int `json:"clientConnectionPort"` -} - -var ( - heartbeatEndpoint string - gsdkConfigFilePath string - sharedContentFolderPath string - certificateFolderPath string - serverLogPath string - vmId string - gamePortsString string - sessionHostId string -) - -func main() { - checkEnvVariables() - - gamePorts, gamePortConfiguration, err := parsePorts() - if err != nil { - log.Fatalf("Could not parse game ports %s", err) - } - - buildMetadata := parseBuildMetadata() - - config := &GsdkConfig{ - HeartbeatEndpoint: heartbeatEndpoint, - SessionHostId: sessionHostId, - VmId: vmId, - LogFolder: serverLogPath, - CertificateFolder: certificateFolderPath, - SharedContentFolder: sharedContentFolderPath, - BuildMetadata: buildMetadata, - GamePorts: gamePorts, - PublicIpV4Address: "N/A", // TODO: can we have that here? - GameServerConnectionInfo: GameServerConnectionInfo{ - PublicIpV4Address: "N/A", - GamePortsConfiguration: gamePortConfiguration, - }, - FullyQualifiedDomainName: "NOT_APPLICABLE", - } - - log.Println("Marshalling to JSON") - configJson, err := json.Marshal(config) - handleError(err) - - log.Println("Getting and creating folder(s)") - folderPath := filepath.Dir(gsdkConfigFilePath) - err = os.MkdirAll(folderPath, os.ModePerm) - handleError(err) - - log.Printf("Creating empty GSDK JSON file %s", gsdkConfigFilePath) - f, err := os.Create(gsdkConfigFilePath) - handleError(err) - - log.Printf("Saving GSDK JSON to file %s", gsdkConfigFilePath) - _, err = f.Write(configJson) - handleError(err) -} - -func parseBuildMetadata() map[string]string { - buildMetadata := make(map[string]string) - if os.Getenv("PF_GAMESERVER_BUILD_METADATA") != "" { - metadata := os.Getenv("PF_GAMESERVER_BUILD_METADATA") - s := strings.Split(metadata, "?") - for _, s2 := range s { - if s2 == "" { - continue - } - s3 := strings.Split(s2, ",") - buildMetadata[s3[0]] = s3[1] - } - } - return buildMetadata -} - -func parsePorts() (map[string]int, []GamePort, error) { - // format is port.Name + "," + containerPort + "," + hostPort + "?" + ... - // similar to how docker run -p works https://docs.docker.com/config/containers/container-networking/ - s := strings.Split(gamePortsString, "?") - gamePortConfiguration := make([]GamePort, 0) - gamePorts := make(map[string]int) - for _, s2 := range s { - if s2 == "" { - continue - } - s3 := strings.Split(s2, ",") - containerPort, err := strconv.Atoi(s3[1]) - if err != nil { - return nil, nil, errors.Wrapf(err, "could not parse port with portName %s, containerPort %s", s3[0], s3[2]) - } - hostPort, err := strconv.Atoi(s3[2]) - if err != nil { - return nil, nil, errors.Wrapf(err, "could not parse port with portName %s, hostPort %s", s3[0], s3[2]) - } - - gamePortConfiguration = append(gamePortConfiguration, GamePort{ - Name: s3[0], - ServerListeningPort: containerPort, - ClientConnectionPort: hostPort, - }) - gamePorts[s3[0]] = containerPort - } - return gamePorts, gamePortConfiguration, nil -} - -func handleError(e error) { - if e != nil { - panic(e) - } -} - -func checkEnvOrFail(envName string, envValue string) { - if envValue == "" { - log.Fatalf("Env %s is empty", envName) - } -} - -func checkEnvVariables() { - heartbeatEndpoint = os.Getenv("HEARTBEAT_ENDPOINT") - checkEnvOrFail("HEARTBEAT_ENDPOINT", heartbeatEndpoint) - - gsdkConfigFilePath = os.Getenv("GSDK_CONFIG_FILE") - checkEnvOrFail("GSDK_CONFIG_FILE", gsdkConfigFilePath) - - sharedContentFolderPath = os.Getenv("PF_SHARED_CONTENT_FOLDER") - checkEnvOrFail("PF_SHARED_CONTENT_FOLDER", sharedContentFolderPath) - - certificateFolderPath = os.Getenv("CERTIFICATE_FOLDER") - checkEnvOrFail("CERTIFICATE_FOLDER", certificateFolderPath) - - serverLogPath = os.Getenv("PF_SERVER_LOG_DIRECTORY") - checkEnvOrFail("PF_SERVER_LOG_DIRECTORY", serverLogPath) - - vmId = os.Getenv("PF_VM_ID") - checkEnvOrFail("PF_VM_ID", vmId) - - gamePortsString = os.Getenv("PF_GAMESERVER_PORTS") - checkEnvOrFail("PF_GAMESERVER_PORTS", gamePortsString) - - sessionHostId = os.Getenv("PF_GAMESERVER_NAME") - checkEnvOrFail("PF_GAMESERVER_NAME", sessionHostId) -} +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +// GsdkConfig is the configuration for the GSDK +// it will be written to the file that will be read by the GSDK running alongside the GameServer +type GsdkConfig struct { + HeartbeatEndpoint string `json:"heartbeatEndpoint"` + SessionHostId string `json:"sessionHostId"` + VmId string `json:"vmId"` + LogFolder string `json:"logFolder"` + CertificateFolder string `json:"certificateFolder"` + SharedContentFolder string `json:"sharedContentFolder"` + BuildMetadata map[string]string `json:"buildMetadata"` + GamePorts map[string]int `json:"gamePorts"` + PublicIpV4Address string `json:"publicIpV4Address"` + GameServerConnectionInfo GameServerConnectionInfo `json:"gameServerConnectionInfo"` + ServerInstanceNumber int `json:"serverInstanceNumber"` // Not used + FullyQualifiedDomainName string `json:"fullyQualifiedDomainName"` +} + +type GameServerConnectionInfo struct { + PublicIpV4Address string `json:"publicIpV4Address"` + GamePortsConfiguration []GamePort `json:"gamePortsConfiguration"` +} + +type GamePort struct { + Name string `json:"name"` + ServerListeningPort int `json:"serverListeningPort"` + ClientConnectionPort int `json:"clientConnectionPort"` +} + +var ( + heartbeatEndpoint string + gsdkConfigFilePath string + sharedContentFolderPath string + certificateFolderPath string + serverLogPath string + vmId string + gamePortsString string + sessionHostId string + crdNamespace string + logger *log.Entry +) + +func main() { + getGameServerNameNamespaceFromEnv() + logger = log.WithFields(log.Fields{"GameServerName": sessionHostId, "GameServerNamespace": crdNamespace}) + + getRestEnvVariables() + + gamePorts, gamePortConfiguration, err := parsePorts() + if err != nil { + logger.Fatalf("Could not parse game ports %s", err) + } + + buildMetadata := parseBuildMetadata() + + config := &GsdkConfig{ + HeartbeatEndpoint: heartbeatEndpoint, + SessionHostId: sessionHostId, + VmId: vmId, + LogFolder: serverLogPath, + CertificateFolder: certificateFolderPath, + SharedContentFolder: sharedContentFolderPath, + BuildMetadata: buildMetadata, + GamePorts: gamePorts, + PublicIpV4Address: "N/A", // TODO: can we have that here? + GameServerConnectionInfo: GameServerConnectionInfo{ + PublicIpV4Address: "N/A", + GamePortsConfiguration: gamePortConfiguration, + }, + FullyQualifiedDomainName: "NOT_APPLICABLE", + } + + logger.Info("Marshalling to JSON") + configJson, err := json.Marshal(config) + handleError(err) + + logger.Info("Getting and creating folder(s)") + folderPath := filepath.Dir(gsdkConfigFilePath) + err = os.MkdirAll(folderPath, os.ModePerm) + handleError(err) + + logger.Infof("Creating empty GSDK JSON file %s", gsdkConfigFilePath) + f, err := os.Create(gsdkConfigFilePath) + handleError(err) + + logger.Infof("Saving GSDK JSON to file %s", gsdkConfigFilePath) + _, err = f.Write(configJson) + handleError(err) +} + +func parseBuildMetadata() map[string]string { + buildMetadata := make(map[string]string) + if os.Getenv("PF_GAMESERVER_BUILD_METADATA") != "" { + metadata := os.Getenv("PF_GAMESERVER_BUILD_METADATA") + s := strings.Split(metadata, "?") + for _, s2 := range s { + if s2 == "" { + continue + } + s3 := strings.Split(s2, ",") + buildMetadata[s3[0]] = s3[1] + } + } + return buildMetadata +} + +func parsePorts() (map[string]int, []GamePort, error) { + // format is port.Name + "," + containerPort + "," + hostPort + "?" + ... + // similar to how docker run -p works https://docs.docker.com/config/containers/container-networking/ + s := strings.Split(gamePortsString, "?") + gamePortConfiguration := make([]GamePort, 0) + gamePorts := make(map[string]int) + for _, s2 := range s { + if s2 == "" { + continue + } + s3 := strings.Split(s2, ",") + containerPort, err := strconv.Atoi(s3[1]) + if err != nil { + return nil, nil, errors.Wrapf(err, "could not parse port with portName %s, containerPort %s", s3[0], s3[2]) + } + hostPort, err := strconv.Atoi(s3[2]) + if err != nil { + return nil, nil, errors.Wrapf(err, "could not parse port with portName %s, hostPort %s", s3[0], s3[2]) + } + + gamePortConfiguration = append(gamePortConfiguration, GamePort{ + Name: s3[0], + ServerListeningPort: containerPort, + ClientConnectionPort: hostPort, + }) + gamePorts[s3[0]] = containerPort + } + return gamePorts, gamePortConfiguration, nil +} + +func handleError(e error) { + if e != nil { + logger.Fatalf("panic because error: %s", e) + } +} + +// checkEnvOrFatal panics if the environment variable is not set +func checkEnvOrFatal(envName string, envValue string) { + if envValue == "" { + logger.Fatalf("Env %s is empty", envName) + } +} + +// getGameServerNameNamespaceFromEnv gets the game server name and namespace from the environment variables +// we get these variables first so we can initialize the logger +func getGameServerNameNamespaceFromEnv() { + sessionHostId = os.Getenv("PF_GAMESERVER_NAME") + if sessionHostId == "" { + panic("PF_GAMESERVER_NAME is empty") + } + + crdNamespace = os.Getenv("PF_GAMESERVER_NAMESPACE") + if crdNamespace == "" { + panic("PF_GAMESERVER_NAMESPACE is empty") + } +} + +// getRestEnvVariables gets the rest environment variables +func getRestEnvVariables() { + heartbeatEndpoint = os.Getenv("HEARTBEAT_ENDPOINT") + checkEnvOrFatal("HEARTBEAT_ENDPOINT", heartbeatEndpoint) + + gsdkConfigFilePath = os.Getenv("GSDK_CONFIG_FILE") + checkEnvOrFatal("GSDK_CONFIG_FILE", gsdkConfigFilePath) + + sharedContentFolderPath = os.Getenv("PF_SHARED_CONTENT_FOLDER") + checkEnvOrFatal("PF_SHARED_CONTENT_FOLDER", sharedContentFolderPath) + + certificateFolderPath = os.Getenv("CERTIFICATE_FOLDER") + checkEnvOrFatal("CERTIFICATE_FOLDER", certificateFolderPath) + + serverLogPath = os.Getenv("PF_SERVER_LOG_DIRECTORY") + checkEnvOrFatal("PF_SERVER_LOG_DIRECTORY", serverLogPath) + + vmId = os.Getenv("PF_VM_ID") + checkEnvOrFatal("PF_VM_ID", vmId) + + gamePortsString = os.Getenv("PF_GAMESERVER_PORTS") + checkEnvOrFatal("PF_GAMESERVER_PORTS", gamePortsString) +} diff --git a/operator/config/manager/kustomization.yaml b/operator/config/manager/kustomization.yaml index 2f53da67..bcb17f90 100755 --- a/operator/config/manager/kustomization.yaml +++ b/operator/config/manager/kustomization.yaml @@ -1,14 +1,14 @@ -resources: -- manager.yaml -generatorOptions: - disableNameSuffixHash: true -configMapGenerator: -- files: - - controller_manager_config.yaml - name: manager-config -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -images: -- name: controller - newName: thundernetes-operator - newTag: 58f6722 +resources: +- manager.yaml +generatorOptions: + disableNameSuffixHash: true +configMapGenerator: +- files: + - controller_manager_config.yaml + name: manager-config +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: thundernetes-operator + newTag: 58f6722 diff --git a/operator/controllers/utilities.go b/operator/controllers/utilities.go index 07ed097f..29d61d4e 100644 --- a/operator/controllers/utilities.go +++ b/operator/controllers/utilities.go @@ -283,6 +283,10 @@ func getInitContainerEnvVariables(gs *mpsv1alpha1.GameServer) []corev1.EnvVar { Name: "PF_GAMESERVER_NAME", // this becomes SessionHostId in gsdkConfig.json file Value: gs.Name, // GameServer.Name is the same as Pod.Name }, + { + Name: "PF_GAMESERVER_NAMESPACE", + Value: gs.Namespace, + }, } var b bytes.Buffer