From 0fef64f2249e10015c657d5ff261013684a59e8e Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:49:50 +0200 Subject: [PATCH] feat: JSON Schemas + editor modelines for module YAML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VS Code's YAML extension guessed a schema from its store and mis-matched template.yaml against the AWS CloudFormation SAM schema, flagging valid fields ("Property apiVersion is not allowed"). Give both module YAML files an explicit schema instead. - .schema/gametemplate.schema.json — generated from the GameTemplate CRD's openAPIV3Schema (template.yaml IS a GameTemplate CR). Regenerated from the Gameplane repo via `make module-schema`. - .schema/module.schema.json — hand-maintained, mirrors module-authoring.md. - Every template.yaml/module.yaml gets a first-line `# yaml-language-server: $schema=…` modeline. The relative ../.schema path resolves whether this repo is opened standalone or as the modules/ submodule. Validated: both schemas accept all 16 modules' files; the modeline is a leading comment so build.sh's parser and validate.py's YAML load are unaffected (.schema/ has no template.yaml, so discover_modules skips it). Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- .schema/README.md | 35 + .schema/gametemplate.schema.json | 2115 +++++++++++++++++++++++++++ .schema/module.schema.json | 62 + 7-days-to-die/module.yaml | 1 + 7-days-to-die/template.yaml | 1 + ark-survival-ascended/module.yaml | 1 + ark-survival-ascended/template.yaml | 1 + cs2/module.yaml | 1 + cs2/template.yaml | 1 + dayz/module.yaml | 1 + dayz/template.yaml | 1 + dont-starve-together/module.yaml | 1 + dont-starve-together/template.yaml | 1 + enshrouded/module.yaml | 1 + enshrouded/template.yaml | 1 + factorio/module.yaml | 1 + factorio/template.yaml | 1 + garrys-mod/module.yaml | 1 + garrys-mod/template.yaml | 1 + minecraft-java/module.yaml | 1 + minecraft-java/template.yaml | 1 + palworld/module.yaml | 1 + palworld/template.yaml | 1 + project-zomboid/module.yaml | 1 + project-zomboid/template.yaml | 1 + rust/module.yaml | 1 + rust/template.yaml | 1 + satisfactory/module.yaml | 1 + satisfactory/template.yaml | 1 + terraria/module.yaml | 1 + terraria/template.yaml | 1 + v-rising/module.yaml | 1 + v-rising/template.yaml | 1 + valheim/module.yaml | 1 + valheim/template.yaml | 1 + 35 files changed, 2244 insertions(+) create mode 100644 .schema/README.md create mode 100644 .schema/gametemplate.schema.json create mode 100644 .schema/module.schema.json diff --git a/.schema/README.md b/.schema/README.md new file mode 100644 index 0000000..ca581a4 --- /dev/null +++ b/.schema/README.md @@ -0,0 +1,35 @@ +# Editor schemas for module YAML + +JSON Schemas that give editors (VS Code's `redhat.vscode-yaml`, and any +`yaml-language-server` client) validation and autocomplete for the two YAML +files in every module — and stop the extension from guessing an unrelated +schema from its schema store (it used to mis-match `template.yaml` against the +AWS SAM schema and flag valid fields). + +Each module file opts in with a first-line modeline: + +```yaml +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # template.yaml +# yaml-language-server: $schema=../.schema/module.schema.json # module.yaml +``` + +The relative path resolves whether this repo is opened on its own or as the +`modules/` submodule of the Gameplane repo. + +| File | Schema | Source | +| --- | --- | --- | +| `template.yaml` | `gametemplate.schema.json` | **Generated** from the GameTemplate CRD in the Gameplane repo. Do not hand-edit. | +| `module.yaml` | `module.schema.json` | Hand-maintained; mirrors `docs/module-authoring.md`. | + +## Regenerating `gametemplate.schema.json` + +`template.yaml` is a `GameTemplate` custom resource, so its schema is the CRD's +`openAPIV3Schema`. When the CRD changes (new spec fields), regenerate from the +**Gameplane** repo — the submodule doesn't carry the CRD: + +```sh +make module-schema # runs hack/gen-module-schema.py, writes modules/.schema/gametemplate.schema.json +``` + +Then commit the updated file here (and bump the submodule pointer in the +Gameplane repo). diff --git a/.schema/gametemplate.schema.json b/.schema/gametemplate.schema.json new file mode 100644 index 0000000..52d461b --- /dev/null +++ b/.schema/gametemplate.schema.json @@ -0,0 +1,2115 @@ +{ + "description": "GameTemplate is a cluster-scoped blueprint for a specific game. A\nGameTemplate is distributed as part of a Gameplane module (OCI artifact)\nand instantiated via GameServer.", + "properties": { + "apiVersion": { + "const": "gameplane.local/v1alpha1", + "description": "gameplane.local/v1alpha1" + }, + "kind": { + "const": "GameTemplate", + "description": "GameTemplate" + }, + "metadata": { + "type": "object" + }, + "spec": { + "description": "GameTemplateSpec defines a reusable blueprint for a game server.\nUsers instantiate a GameTemplate by creating a GameServer that\nreferences it.", + "properties": { + "accentColor": { + "description": "AccentColor is an optional brand color (CSS hex, e.g. \"#3b82f6\")\nthe dashboard uses to tint this game's icon and accents. When\nempty the dashboard falls back to a neutral default. This replaces\nthe previously hardcoded per-game color palette in the web app, so\nnew games carry their own color without a frontend change.", + "pattern": "^#[0-9a-fA-F]{6}$", + "type": "string" + }, + "agent": { + "description": "Agent tunes the sidecar deployed alongside the game container.", + "properties": { + "image": { + "description": "Image overrides the default agent image. Normally set by the\noperator to the image matching its own build.", + "type": "string" + }, + "resources": { + "description": "Resources overrides agent resources.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims,\nthat are used by this container.\n\nThis field depends on the\nDynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of\nthe Pod where this field is used. It makes that resource available\ninside a container.", + "type": "string" + }, + "request": { + "description": "Request is the name chosen for a request in the referenced claim.\nIf empty, everything from the claim is made available, otherwise\nonly the result of this request.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "description": "Limits describes the maximum amount of compute resources allowed.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + }, + "requests": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "description": "Requests describes the minimum amount of compute resources required.\nIf Requests is omitted for a container, it defaults to Limits if that is explicitly specified,\notherwise to an implementation-defined value. Requests cannot exceed Limits.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "args": { + "items": { + "type": "string" + }, + "type": "array" + }, + "capabilities": { + "description": "Capabilities declares the game-specific console commands behind\nagent features (player moderation, backup quiesce). The operator\nserializes this onto the agent sidecar, which interprets it at\nruntime \u2014 modules add full feature support without agent code\nchanges. All commands run over the template's RCON connection,\nso they require rcon.protocol != none.", + "properties": { + "actions": { + "description": "Actions declares named operator actions surfaced as buttons on the\nserver detail page. Each runs a templated console command over the\ntemplate's RCON connection, so they require rcon.protocol != none.", + "items": { + "description": "ServerActionSpec declares a named operator action surfaced as a button\non the server detail page. Running it renders Command (a Go\ntext/template) with the collected parameters and sends the result over\nthe template's RCON connection.", + "properties": { + "command": { + "description": "Command is a Go text/template rendered with .Params (each declared\nparameter name mapped to its resolved value) and sent over RCON,\ne.g. \"say {{.Params.message}}\". Parameter values are sanitized for\nconsole-injection before rendering.", + "minLength": 1, + "type": "string" + }, + "commands": { + "description": "Commands runs several console commands in order (mutually exclusive\nwith Command). Mirrors capabilities.quiesce / lifecycle.stop, which\nalready express sequences as a plain []string. Each is a Go template\nrendered with .Params, same as Command.", + "items": { + "type": "string" + }, + "maxItems": 16, + "type": "array" + }, + "confirm": { + "description": "Confirm, when true, makes the dashboard require explicit\nconfirmation before running the action.", + "type": "boolean" + }, + "danger": { + "description": "Danger marks a destructive action so the dashboard styles it\ndistinctly (e.g. a red button).", + "type": "boolean" + }, + "description": { + "description": "Description explains the action; shown in the confirmation dialog.", + "type": "string" + }, + "displayName": { + "description": "DisplayName is the button label.", + "minLength": 1, + "type": "string" + }, + "group": { + "description": "Group labels a section on the server detail page (e.g. \"World\").", + "maxLength": 24, + "type": "string" + }, + "icon": { + "description": "Icon is an optional lucide-react icon name (e.g. \"megaphone\").", + "type": "string" + }, + "id": { + "description": "ID is a stable identifier, unique within the template's actions.", + "maxLength": 63, + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", + "type": "string" + }, + "params": { + "description": "Params declares inputs collected from the user before running.", + "items": { + "description": "ActionParamSpec is a single input collected before running an action.", + "properties": { + "default": { + "description": "Default is the pre-filled value (as a string).", + "type": "string" + }, + "description": { + "description": "Description explains the input.", + "type": "string" + }, + "displayName": { + "description": "DisplayName labels the input in the UI.", + "type": "string" + }, + "enum": { + "description": "Enum restricts valid values when Type=enum.", + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "description": "Name is the parameter identifier referenced in the command\ntemplate as {{.Params.}}.", + "maxLength": 63, + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$", + "type": "string" + }, + "required": { + "description": "Required, when true, blocks running until the input is set.", + "type": "boolean" + }, + "type": { + "default": "string", + "description": "Type controls the input widget and validation.", + "enum": [ + "string", + "int", + "bool", + "enum" + ], + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "type": "object" + }, + "maxItems": 16, + "type": "array" + }, + "transport": { + "description": "Transport selects how the commands reach the game. Empty resolves to\nrcon when rcon.protocol != none, else stdin (pod-attach) \u2014 so a\npty-console game like Terraria can carry actions too.", + "enum": [ + "rcon", + "stdin" + ], + "type": "string" + } + }, + "required": [ + "displayName", + "id" + ], + "type": "object", + "x-kubernetes-validations": [ + { + "message": "set exactly one of command or commands", + "rule": "has(self.command) != (has(self.commands) && size(self.commands) > 0)" + } + ] + }, + "maxItems": 32, + "type": "array" + }, + "lifecycle": { + "description": "Lifecycle configures graceful lifecycle transitions the agent drives\nover RCON \u2014 currently the in-game stop sequence the operator runs\nbefore scaling the server down, so the world is saved and shut down\ncleanly instead of relying on a container SIGTERM. Requires\nrcon.protocol != none.", + "properties": { + "stop": { + "description": "Stop runs, in order, to shut the game down cleanly before the\noperator scales the server to zero (e.g. [\"stop\"] for Minecraft, or\na warn-then-shutdown sequence). The operator issues these, waits for\nthe game to go not-ready (or for GameServer.spec.stopGracePeriodSeconds\nto elapse), then removes the pod \u2014 so a SIGTERM never interrupts an\nin-progress save.", + "items": { + "type": "string" + }, + "maxItems": 16, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "stop" + ], + "type": "object" + }, + "mods": { + "description": "Mods declares how the dashboard manages this game's mods/plugins.\nThe dashboard is generic \u2014 it lists, installs, and removes mods by\ncalling the agent; this block tells the agent where mods live and\nhow installs are permitted. Mods are files under a directory on the\ngame's data volume, so this needs no RCON.", + "properties": { + "extensions": { + "description": "Extensions optionally restricts which files in Path are treated as\nmods (e.g. [\".jar\"]). Empty lists every file. Used with Path; per-\nloader extensions are set on each ModLoaderSpec instead.", + "items": { + "type": "string" + }, + "type": "array" + }, + "extract": { + "description": "Extract is the resolved per-loader Extract flag (the operator copies\nit from the active loader). When true the agent unpacks archive mods\ninto per-mod folders. Not normally set by template authors at this\nlevel \u2014 set it on the loader entry instead.", + "type": "boolean" + }, + "idList": { + "description": "IDList declares that this game installs mods by ID rather than by file:\nthe server downloads its own mods given a list of ids, so the operator\nprojects the GameServer's selected ids into a game-container env var\ninstead of the agent downloading files into a mods directory.\nMutually exclusive with Path/Loaders in practice \u2014 a game does one or\nthe other.", + "properties": { + "env": { + "description": "Env is the game-container environment variable the rendered id list\nis projected into (e.g. \"ASA_START_PARAMS\", \"MOD_IDS\").", + "maxLength": 64, + "minLength": 1, + "type": "string" + }, + "format": { + "default": "{{ids}}", + "description": "Format renders the joined id list into the env value. The literal\ntoken \"{{ids}}\" is replaced with the separator-joined ids. This is a\nplain string replacement, NOT a Go text/template \u2014 a module bundle\nmust not be able to execute template logic through this field.\nDefaults to \"{{ids}}\". ARK uses \" -mods={{ids}}\" so the ids append\nonto its existing launch parameters.", + "maxLength": 128, + "type": "string" + }, + "mode": { + "default": "replace", + "description": "Mode is how the rendered value is applied to Env:\n\"replace\" (default) sets Env to the rendered value;\n\"append\" concatenates the rendered value onto Env's existing value\n(used when the same env also carries user-supplied launch params).", + "enum": [ + "replace", + "append" + ], + "type": "string" + }, + "separator": { + "default": ",", + "description": "Separator joins the ids. Defaults to \",\".", + "maxLength": 4, + "type": "string" + } + }, + "required": [ + "env" + ], + "type": "object" + }, + "install": { + "description": "Install, when set, lets the dashboard add new mods by downloading\nthem into the resolved mods directory. When unset, only listing and\nremoval are offered. Shared across all loaders.", + "properties": { + "allowedHosts": { + "description": "AllowedHosts is the allowlist of hosts the agent may download from:\nan exact hostname (\"cdn.modrinth.com\") or a leading-dot suffix\n(\".modrinth.com\") matching that host and its subdomains. Downloads\nfrom any other host \u2014 and any that resolve to a private, loopback,\nor link-local address \u2014 are refused (SSRF guard).", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "maxSizeMB": { + "description": "MaxSizeMB caps the download size in mebibytes. Defaults to 256.", + "format": "int32", + "maximum": 4096, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "allowedHosts" + ], + "type": "object" + }, + "loaders": { + "additionalProperties": { + "description": "ModLoaderSpec is the mods directory for one loader / server-type,\nselected by the active GameVersion.loader. The operator provisions a\nper-(version+loader) PVC and mounts it at storage.mountPath/ on\nboth the game container (where the image reads mods) and the agent.", + "properties": { + "displayName": { + "description": "DisplayName labels this volume in the Mods tab, e.g. \"Plugins (Paper)\".", + "type": "string" + }, + "extensions": { + "description": "Extensions optionally restricts which files are treated as mods for\nthis loader (e.g. [\".jar\"], [\".dll\"]). Empty lists every file.", + "items": { + "type": "string" + }, + "type": "array" + }, + "extract": { + "description": "Extract, when true, tells the agent to treat downloaded mods as\narchives (e.g. Thunderstore .zip): each install unpacks into its own\nfolder under the mods dir so the loader (e.g. BepInEx, which scans\nrecursively) finds the contained files. Listing/removal then operate\non those per-mod folders. Use for loaders distributed as archives.", + "type": "boolean" + }, + "path": { + "description": "Path is this loader's mods/plugins directory, relative to\nstorage.mountPath (e.g. \"plugins\", \"mods\", \"bepinex/plugins\",\n\"ModPacks\"). Absolute paths and \"..\" segments are rejected. MaxLength\nis kept small to bound the per-entry CEL validation cost (this rule\nlives inside the loaders map, so its cost is multiplied per entry).", + "maxLength": 128, + "minLength": 1, + "type": "string", + "x-kubernetes-validations": [ + { + "message": "path must be relative to the data mount and must not contain '..'", + "rule": "!self.startsWith('/') && !self.contains('..')" + } + ] + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "description": "Loaders maps a loader id (from GameVersion.loader) to its mods\ndirectory. When the active version's loader has an entry here, the\noperator provisions a per-(version+loader) PVC, mounts it at\nstorage.mountPath/ on the game and agent containers, and points\nthe agent's mod manager at it \u2014 so each version+loader keeps its own\nmod set. When empty, Path is the single shared mods dir (legacy).\nMaxProperties bounds the per-entry CEL validation cost (the apiserver\nrejects the CRD otherwise).", + "maxProperties": 16, + "type": "object" + }, + "path": { + "description": "Path is the single shared mods directory, relative to\nstorage.mountPath (e.g. \"mods\" for Forge/Fabric, \"plugins\" for\nBukkit/Paper). Used when Loaders is empty. Absolute paths and \"..\"\nsegments are rejected.", + "maxLength": 256, + "type": "string", + "x-kubernetes-validations": [ + { + "message": "path must be relative to the data mount and must not contain '..'", + "rule": "!self.startsWith('/') && !self.contains('..')" + } + ] + }, + "registry": { + "description": "Registry, when set, lets the dashboard browse and search an external\nmod registry for this game (in addition to install-by-URL). Gameplane\nships the provider engines and a generic browse UI; the module selects\na provider here and the agent's Install downloads the chosen file \u2014\nso the registry's CDN must also be in Install.AllowedHosts. Omit for\nURL-only games (e.g. tModLoader, whose mods live on Steam Workshop).", + "properties": { + "providers": { + "description": "Providers is the ordered list of registries to offer. The dashboard\nshows a provider switch when there's more than one; the first is the\ndefault. A provider whose engine needs unmet config (e.g. a\nCurseForge API key) is hidden until configured.", + "items": { + "description": "ModProvider configures one registry engine for a game.", + "properties": { + "community": { + "description": "Community is the Thunderstore community slug whose package index to\nsearch, e.g. \"valheim\" (required by the thunderstore provider), or\nthe Nexus Mods game domain slug, e.g. \"skyrimspecialedition\"\n(required by the nexus provider). Ignored by others.", + "maxLength": 64, + "type": "string" + }, + "credentialsSecretRef": { + "description": "CredentialsSecretRef references a Secret in the GameServer's namespace\nthat holds registry download credentials. The Secret must contain\n\"username\" and \"token\" keys. Currently used by the \"factorio\" provider\nfor mod-portal downloads. The agent injects these credentials\ntransparently during installs.", + "properties": { + "name": { + "maxLength": 253, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "github": { + "description": "GitHub binds this provider to one repository's Releases. GitHub has\nno cross-repo mod search (unlike Thunderstore's per-community\nindex), so a template picks exactly one repo to browse. Required\nwhen Provider is \"github\"; ignored otherwise.", + "properties": { + "owner": { + "description": "Owner is the repository owner (user or organization login).", + "maxLength": 39, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", + "type": "string" + }, + "repo": { + "description": "Repo is the repository name.", + "maxLength": 100, + "minLength": 1, + "pattern": "^[A-Za-z0-9._-]+$", + "type": "string", + "x-kubernetes-validations": [ + { + "message": "repo must not contain '..'", + "rule": "!self.contains('..')" + } + ] + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "modpacks": { + "description": "Modpacks, when set, surfaces a Modpacks browser for this provider and\ndeclares how installing one is applied. A modpack is selected as a\nwhole (not added to the mods dir like a single mod), so install\neither pins it via a game-image env (RefEnv) or resolves and installs\nits dependency mods. Omit for providers without modpacks. Steam\nWorkshop collections are always env-mode: set RefEnv to the game\nimage's collection-id variable (e.g. \"WORKSHOP_COLLECTION\") since\nsteamcmd resolves a collection's members itself inside the container.", + "properties": { + "env": { + "description": "Env are additional fixed env applied alongside RefEnv when a modpack\nis active, e.g. {TYPE: MODRINTH} to switch the itzg image into\nModrinth-modpack mode. Bounded to keep the CRD small.", + "items": { + "description": "EnvVar represents an environment variable present in a Container.", + "properties": { + "name": { + "description": "Name of the environment variable.\nMay consist of any printable ASCII characters except '='.", + "type": "string" + }, + "value": { + "description": "Variable references $(VAR_NAME) are expanded\nusing the previously defined environment variables in the container and\nany service environment variables. If a variable cannot be resolved,\nthe reference in the input string will be unchanged. Double $$ are reduced\nto a single $, which allows for escaping the $(VAR_NAME) syntax: i.e.\n\"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\".\nEscaped references will never be expanded, regardless of whether the variable\nexists or not.\nDefaults to \"\".", + "type": "string" + }, + "valueFrom": { + "description": "Source for the environment variable's value. Cannot be used if value is not empty.", + "properties": { + "configMapKeyRef": { + "description": "Selects a key of a ConfigMap.", + "properties": { + "key": { + "description": "The key to select.", + "type": "string" + }, + "name": { + "default": "", + "description": "Name of the referent.\nThis field is effectively required, but due to backwards compatibility is\nallowed to be empty. Instances of this type with an empty value here are\nalmost certainly wrong.\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "Specify whether the ConfigMap or its key must be defined", + "type": "boolean" + } + }, + "required": [ + "key" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "fieldRef": { + "description": "Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`,\nspec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": "string" + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "fileKeyRef": { + "description": "FileKeyRef selects a key of the env file.\nRequires the EnvFiles feature gate to be enabled.", + "properties": { + "key": { + "description": "The key within the env file. An invalid key will prevent the pod from starting.\nThe keys defined within a source may consist of any printable ASCII characters except '='.\nDuring Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters.", + "type": "string" + }, + "optional": { + "default": false, + "description": "Specify whether the file or its key must be defined. If the file or key\ndoes not exist, then the env var is not published.\nIf optional is set to true and the specified key does not exist,\nthe environment variable will not be set in the Pod's containers.\n\nIf optional is set to false and the specified key does not exist,\nan error will be returned during Pod creation.", + "type": "boolean" + }, + "path": { + "description": "The path within the volume from which to select the file.\nMust be relative and may not contain the '..' path or start with '..'.", + "type": "string" + }, + "volumeName": { + "description": "The name of the volume mount containing the env file.", + "type": "string" + } + }, + "required": [ + "key", + "path", + "volumeName" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "resourceFieldRef": { + "description": "Selects a resource of the container: only resources limits and requests\n(limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": "string" + }, + "divisor": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "Specifies the output format of the exposed resources, defaults to \"1\"", + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "secretKeyRef": { + "description": "Selects a key of a secret in the pod's namespace", + "properties": { + "key": { + "description": "The key of the secret to select from. Must be a valid secret key.", + "type": "string" + }, + "name": { + "default": "", + "description": "Name of the referent.\nThis field is effectively required, but due to backwards compatibility is\nallowed to be empty. Instances of this type with an empty value here are\nalmost certainly wrong.\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "Specify whether the Secret or its key must be defined", + "type": "boolean" + } + }, + "required": [ + "key" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + } + }, + "type": "object" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "maxItems": 16, + "type": "array" + }, + "refEnv": { + "description": "RefEnv, when set, is the game-image env the operator points at the\nchosen modpack reference (slug/URL) \u2014 e.g. \"MODRINTH_MODPACK\" for the\nitzg image. Installing then patches GameServer.spec.env and restarts;\none modpack is active per server. When empty, installing instead\nresolves and installs the modpack's dependency mods (e.g. a\nThunderstore/BepInEx pack).", + "maxLength": 64, + "type": "string" + } + }, + "type": "object" + }, + "provider": { + "description": "Provider names the built-in registry engine: \"modrinth\" (Minecraft\nmods/plugins, keyless), \"thunderstore\" (BepInEx games, keyless,\nper-community), \"curseforge\" (Minecraft mods/modpacks, needs an API\nkey), \"hangar\" (PaperMC plugins, keyless), \"factorio\" (the official\nFactorio mod portal; browse is keyless, downloads need the player's\nown factorio.com credentials so installs hand off to the from-URL\nform), \"steam\" (Steam Workshop browse, needs a Steam Web API key;\nsee SteamAppID \u2014 Workshop content has no download URL, so it's a\npreview-only browser wired to modpacks.refEnv for collection-based\ngames like Garry's Mod/CS2), \"nexus\" (Nexus Mods, needs an API key,\nbrowse-only for the same reason as steam \u2014 see Community for its\nper-game domain slug), \"spigot\" (SpigotMC plugins via the Spiget API,\nkeyless), \"github\" (one repository's Releases stand in for\nversions, keyless but rate-limited \u2014 see GitHub), or \"umod\"\n(Rust/Hurtworld/7 Days to Die's Oxide/uMod plugin ecosystem,\nkeyless).", + "enum": [ + "modrinth", + "thunderstore", + "curseforge", + "hangar", + "factorio", + "steam", + "spigot", + "github", + "umod", + "nexus" + ], + "type": "string" + }, + "steamAppID": { + "description": "SteamAppID is the Steam application id this provider browses (e.g.\n4000 for Garry's Mod, 730 for Counter-Strike 2) \u2014 it facets the\nWorkshop search to that app's items/collections. Required when\nProvider is \"steam\"; ignored otherwise.", + "format": "int32", + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "provider" + ], + "type": "object", + "x-kubernetes-validations": [ + { + "message": "steamAppID is required when provider is steam", + "rule": "self.provider != 'steam' || (has(self.steamAppID) && self.steamAppID > 0)" + }, + { + "message": "github is required when provider is github", + "rule": "self.provider != 'github' || has(self.github)" + } + ] + }, + "maxItems": 8, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "providers" + ], + "type": "object" + } + }, + "type": "object", + "x-kubernetes-validations": [ + { + "message": "mods requires path, at least one loaders entry, or idList", + "rule": "has(self.path) || (has(self.loaders) && size(self.loaders) > 0) || has(self.idList)" + } + ] + }, + "players": { + "description": "Players configures the moderation actions on the Players tab.", + "properties": { + "ban": { + "description": "Ban bans a player.", + "type": "string" + }, + "banList": { + "description": "BanList configures reading the current ban list.", + "properties": { + "command": { + "description": "Command prints the ban list (e.g. \"banlist players\").", + "minLength": 1, + "type": "string" + }, + "entryRegex": { + "description": "EntryRegex matches one banned player per output line, using the\nnamed groups \"name\" (required), \"source\" and \"reason\" (optional).", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "command", + "entryRegex" + ], + "type": "object" + }, + "kick": { + "description": "Kick disconnects a player, e.g.\n\"kick {{.Player}}{{if .Reason}} {{.Reason}}{{end}}\".", + "type": "string" + }, + "list": { + "description": "List configures how the online player list is read.", + "properties": { + "command": { + "description": "Command is the console/RCON command that prints the online players (e.g. \"list\").", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "entryRegex": { + "description": "EntryRegex optionally extracts one player name per match from the\ncommand output (first capture group, or whole match if no group).\n^ and $ match per line (multiline mode).\nWhen empty the built-in parser is used.", + "maxLength": 512, + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + }, + "unban": { + "description": "Unban lifts a ban, e.g. \"pardon {{.Player}}\".", + "type": "string" + }, + "whitelist": { + "description": "Whitelist configures the allow-list management actions on the\nPlayers tab (list / add / remove).", + "properties": { + "add": { + "description": "Add adds a player, e.g. \"whitelist add {{.Player}}\".", + "minLength": 1, + "type": "string" + }, + "list": { + "description": "List prints the current whitelist (e.g. \"whitelist list\").", + "minLength": 1, + "type": "string" + }, + "listRegex": { + "description": "ListRegex extracts whitelisted names from the List output. If it has\na named group \"names\", that group is split on commas (Minecraft's\n\"There are N whitelisted players: a, b, c\"); otherwise each line's\n\"name\" group is one entry.", + "minLength": 1, + "type": "string" + }, + "remove": { + "description": "Remove removes a player, e.g. \"whitelist remove {{.Player}}\".", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "add", + "list", + "listRegex", + "remove" + ], + "type": "object" + } + }, + "type": "object" + }, + "quiesce": { + "description": "Quiesce configures how in-flight game state is flushed to disk\nbefore a backup snapshot (and resumed afterwards).", + "properties": { + "failurePattern": { + "description": "FailurePattern, when it matches a quiesce command's output\n(case-insensitive regex), treats the step as failed even though\nthe command itself returned successfully.", + "type": "string" + }, + "quiesce": { + "description": "Quiesce runs before the snapshot, in order (e.g. [\"save-off\",\n\"save-all flush\"]). Any command error triggers a best-effort\nUnquiesce.", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "unquiesce": { + "description": "Unquiesce runs after the snapshot (e.g. [\"save-on\"]).", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "quiesce", + "unquiesce" + ], + "type": "object" + }, + "status": { + "description": "Status declares game-specific live metrics shown on the Overview\ntab, each read via an RCON command parsed by a named-group regex.\nLike actions, they require rcon.protocol != none.", + "properties": { + "metrics": { + "description": "Metrics are the per-game readouts. Each runs an RCON command and\nextracts a value via a named-group regex.", + "items": { + "description": "StatusMetricSpec reads one live metric from an RCON command's output.", + "properties": { + "command": { + "description": "Command is the RCON command whose output is parsed.", + "minLength": 1, + "type": "string" + }, + "displayName": { + "description": "DisplayName labels the metric in the UI.", + "minLength": 1, + "type": "string" + }, + "id": { + "description": "ID is a stable identifier, unique within the template's metrics.", + "maxLength": 63, + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", + "type": "string" + }, + "regex": { + "description": "Regex extracts the value via the named group \"value\", e.g.\n`TPS: (?P[0-9.]+)`.", + "minLength": 1, + "type": "string" + }, + "unit": { + "description": "Unit is an optional suffix shown after the value (e.g. \"ms\", \"TPS\").", + "type": "string" + } + }, + "required": [ + "command", + "displayName", + "id", + "regex" + ], + "type": "object" + }, + "maxItems": 16, + "type": "array" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "categories": { + "description": "Categories group this game in the dashboard's catalog and Create\nServer picker (e.g. [\"Survival\", \"Sandbox\"]). A game may belong to\nseveral at once \u2014 Minecraft is reasonably Sandbox, Survival and\nCreative. The dashboard builds its category filter from the distinct\nvalues present across installed templates, so a module introduces a\nnew category simply by naming it here \u2014 no frontend change. Empty\nfalls back to a heuristic on the game slug, and finally to \"Other\".\ndocs/module-authoring.md publishes the canonical vocabulary the\nofficial modules use.", + "items": { + "maxLength": 32, + "minLength": 1, + "type": "string" + }, + "maxItems": 8, + "type": "array" + }, + "command": { + "description": "Command / Args override the container image entrypoint when set.", + "items": { + "type": "string" + }, + "type": "array" + }, + "configFiles": { + "description": "ConfigFiles declares files the operator renders from the resolved\nconfig values and places under Storage.MountPath before the game\nstarts. Rendered contents are stored in an owned `-files`\nSecret (they may embed password values) and copied onto the data\nvolume by an init container on every pod start \u2014 manual edits to\nthese paths via the Files tab are overwritten on restart.", + "items": { + "description": "ConfigFile is a single operator-rendered file on the game's data\nvolume.", + "properties": { + "path": { + "description": "Path is where the rendered file lands, relative to\nStorage.MountPath (e.g. \"serverconfig.txt\", \"cfg/server.cfg\").\nAbsolute paths and \"..\" segments are rejected.", + "maxLength": 512, + "minLength": 1, + "type": "string", + "x-kubernetes-validations": [ + { + "message": "path must be relative to the data mount and must not contain '..'", + "rule": "!self.startsWith('/') && !self.contains('..')" + } + ] + }, + "template": { + "description": "Template is a Go text/template rendered with `.Values` (every\nconfigSchema field name mapped to its resolved value, \"\" when an\noptional field is unset) and `.Server` (`.Name`, `.Namespace`).\nRendering uses missingkey=error: referencing a key outside the\nschema fails the GameServer.", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "path", + "template" + ], + "type": "object" + }, + "maxItems": 32, + "type": "array" + }, + "configSchema": { + "description": "ConfigSchema declares user-tunable fields surfaced in the Create\nServer wizard. The operator resolves GameServer.spec.config\nagainst this schema (applying defaults, validating types/enums)\nand sets each resolved value as an env var on the game container.\nFields with target=file are consumed by ConfigFiles templates\ninstead of becoming env vars.", + "items": { + "description": "ConfigField is a single user-tunable setting surfaced in the wizard.", + "properties": { + "autoFromMemoryLimit": { + "description": "AutoFromMemoryLimit, when set, computes this field's value from the\ngame container's effective memory limit whenever neither\nGameServer.spec.config nor Default provides one: floor(limit \u00d7\npercent / 100), rendered as whole mebibytes with an \"M\" suffix\n(e.g. \"6144M\"). Lets templates size game memory settings (like a\nJVM heap) to whatever resources the server was given instead of a\nstatic default \u2014 an explicit user value always wins, and a template\nwithout a memory limit leaves the field unset.", + "properties": { + "percent": { + "description": "Percent of the memory limit the computed value takes. Values well\nbelow 100 leave headroom for non-heap memory \u2014 e.g. a JVM heap at\n75% of the container limit avoids the OOM kill that a heap at or\nabove the limit invites.", + "format": "int32", + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "percent" + ], + "type": "object" + }, + "default": { + "description": "Default is the default value (as a string) rendered in the wizard.", + "type": "string" + }, + "description": { + "description": "Description explains the field to end users.", + "type": "string" + }, + "displayName": { + "description": "DisplayName is shown in the UI.", + "type": "string" + }, + "enum": { + "description": "Enum restricts valid values when Type=enum.", + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "description": "Name is the field identifier (also used as an env var when\nTarget is \"env\").", + "minLength": 1, + "type": "string" + }, + "required": { + "description": "Required, when true, blocks wizard submission if unset.", + "type": "boolean" + }, + "target": { + "default": "env", + "description": "Target controls where the value is applied. \"env\" (default) sets\nan env var on the game container; \"file\" makes the value available\nto spec.configFiles templates instead of the environment.", + "enum": [ + "env", + "file" + ], + "type": "string" + }, + "type": { + "default": "string", + "description": "Type controls the input widget in the wizard. Values of password\nfields are stored in a per-GameServer Secret and injected via\nSecretKeyRef, never inline in the pod spec.", + "enum": [ + "string", + "int", + "bool", + "enum", + "password" + ], + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "type": "object" + }, + "type": "array" + }, + "consoleMode": { + "description": "ConsoleMode controls how the dashboard's Console tab attaches to\nthe running game.\n\n - \"rcon\": send line-based RCON commands (default when RCON is set\n and its protocol is not \"none\").\n - \"pty\": attach to the game container's stdin/stdout via the\n Kubernetes pod-attach API. Requires the container to be started\n with tty=true and stdin=true (the operator sets these). Switching\n to pty after the pod exists requires a pod recreate.\n - \"none\": disable the Console tab for this game.\n\nWhen unset, the operator defaults to \"rcon\" if RCON.Protocol is set\nto anything other than \"none\", and \"none\" otherwise.", + "enum": [ + "rcon", + "pty", + "none" + ], + "type": "string" + }, + "description": { + "description": "Description is free-form markdown describing the template.", + "type": "string" + }, + "displayName": { + "description": "DisplayName is a human-friendly label shown in the dashboard.", + "minLength": 1, + "type": "string" + }, + "env": { + "description": "Env is the default environment for the game container.", + "items": { + "description": "EnvVar represents an environment variable present in a Container.", + "properties": { + "name": { + "description": "Name of the environment variable.\nMay consist of any printable ASCII characters except '='.", + "type": "string" + }, + "value": { + "description": "Variable references $(VAR_NAME) are expanded\nusing the previously defined environment variables in the container and\nany service environment variables. If a variable cannot be resolved,\nthe reference in the input string will be unchanged. Double $$ are reduced\nto a single $, which allows for escaping the $(VAR_NAME) syntax: i.e.\n\"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\".\nEscaped references will never be expanded, regardless of whether the variable\nexists or not.\nDefaults to \"\".", + "type": "string" + }, + "valueFrom": { + "description": "Source for the environment variable's value. Cannot be used if value is not empty.", + "properties": { + "configMapKeyRef": { + "description": "Selects a key of a ConfigMap.", + "properties": { + "key": { + "description": "The key to select.", + "type": "string" + }, + "name": { + "default": "", + "description": "Name of the referent.\nThis field is effectively required, but due to backwards compatibility is\nallowed to be empty. Instances of this type with an empty value here are\nalmost certainly wrong.\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "Specify whether the ConfigMap or its key must be defined", + "type": "boolean" + } + }, + "required": [ + "key" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "fieldRef": { + "description": "Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`,\nspec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": "string" + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "fileKeyRef": { + "description": "FileKeyRef selects a key of the env file.\nRequires the EnvFiles feature gate to be enabled.", + "properties": { + "key": { + "description": "The key within the env file. An invalid key will prevent the pod from starting.\nThe keys defined within a source may consist of any printable ASCII characters except '='.\nDuring Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters.", + "type": "string" + }, + "optional": { + "default": false, + "description": "Specify whether the file or its key must be defined. If the file or key\ndoes not exist, then the env var is not published.\nIf optional is set to true and the specified key does not exist,\nthe environment variable will not be set in the Pod's containers.\n\nIf optional is set to false and the specified key does not exist,\nan error will be returned during Pod creation.", + "type": "boolean" + }, + "path": { + "description": "The path within the volume from which to select the file.\nMust be relative and may not contain the '..' path or start with '..'.", + "type": "string" + }, + "volumeName": { + "description": "The name of the volume mount containing the env file.", + "type": "string" + } + }, + "required": [ + "key", + "path", + "volumeName" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "resourceFieldRef": { + "description": "Selects a resource of the container: only resources limits and requests\n(limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": "string" + }, + "divisor": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "Specifies the output format of the exposed resources, defaults to \"1\"", + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "secretKeyRef": { + "description": "Selects a key of a secret in the pod's namespace", + "properties": { + "key": { + "description": "The key of the secret to select from. Must be a valid secret key.", + "type": "string" + }, + "name": { + "default": "", + "description": "Name of the referent.\nThis field is effectively required, but due to backwards compatibility is\nallowed to be empty. Instances of this type with an empty value here are\nalmost certainly wrong.\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "Specify whether the Secret or its key must be defined", + "type": "boolean" + } + }, + "required": [ + "key" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + } + }, + "type": "object" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + }, + "game": { + "description": "Game is the canonical game identifier (e.g. \"minecraft-java\",\n\"valheim\", \"factorio\"). Used as a grouping key in the UI catalog.", + "minLength": 1, + "type": "string" + }, + "icon": { + "description": "Icon is an optional URL or data URI shown in the catalog.", + "type": "string" + }, + "image": { + "description": "Image is the default container image (e.g.\n\"itzg/minecraft-server:2025.1.0\"). Used as the fallback when the\nserver selects no version (see Versions) and sets no spec.image\noverride. Can be overridden by GameServer.", + "minLength": 1, + "type": "string" + }, + "logPath": { + "description": "LogPath is the file holding the game's primary log, as seen from\ninside the pod (e.g. \"/data/logs/latest.log\"). It must live under\nthe shared data volume (Storage.MountPath) so the agent sidecar\ncan tail it for the dashboard's Logs tab. Leave empty for games\nthat only log to stdout \u2014 the Logs tab is then unavailable.", + "type": "string" + }, + "ports": { + "description": "Ports declares exposed ports on the game container.", + "items": { + "description": "GamePort is a single exposed port.", + "properties": { + "advertise": { + "default": true, + "description": "Advertise controls whether this port is exposed to users via\nGameServer.Spec.Networking. RCON and query ports are typically\nnot advertised publicly.\n\nNo `omitempty`: the CRD default is true, so an `omitempty` tag would\nlet the typed client silently drop an explicit `false` on the wire\n(Go's zero value for bool), and the apiserver would re-apply the\n`true` default \u2014 putting RCON/query ports back on the Service and\nthe game-ingress NetworkPolicy. Do not add it back.", + "type": "boolean" + }, + "containerPort": { + "description": "ContainerPort is the port the game listens on inside the pod.", + "format": "int32", + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "name": { + "description": "Name is a DNS-label port identifier (e.g. \"game\", \"query\", \"rcon\").", + "minLength": 1, + "type": "string" + }, + "protocol": { + "default": "TCP", + "description": "Protocol is one of TCP, UDP. Defaults to TCP.", + "enum": [ + "TCP", + "UDP" + ], + "type": "string" + } + }, + "required": [ + "containerPort", + "name" + ], + "type": "object" + }, + "type": "array" + }, + "probes": { + "description": "Probes are the default readiness/liveness/startup probes for the\ngame container. Operator supplies sane defaults when unset.", + "properties": { + "liveness": { + "description": "Probe describes a health check to be performed against a container to determine whether it is\nalive or ready to receive traffic.", + "properties": { + "exec": { + "description": "Exec specifies a command to execute in the container.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the\ncommand is root ('/') in the container's filesystem. The command is simply exec'd, it is\nnot run inside a shell, so traditional shell instructions ('|', etc) won't work. To use\na shell, you need to explicitly call out to that shell.\nExit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded.\nDefaults to 3. Minimum value is 1.", + "format": "int32", + "type": "integer" + }, + "grpc": { + "description": "GRPC specifies a GRPC HealthCheckRequest.", + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "default": "", + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest\n(see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": "string" + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "httpGet": { + "description": "HTTPGet specifies an HTTP GET request to perform.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set\n\"Host\" in httpHeaders instead.", + "type": "string" + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name.\nThis will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": "string" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "Name or number of the port to access on the container.\nNumber must be in the range 1 to 65535.\nName must be an IANA_SVC_NAME.", + "x-kubernetes-int-or-string": true + }, + "scheme": { + "description": "Scheme to use for connecting to the host.\nDefaults to HTTP.", + "type": "string" + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated.\nMore info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": "integer" + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe.\nDefault to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": "integer" + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed.\nDefaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": "integer" + }, + "tcpSocket": { + "description": "TCPSocket specifies a connection to a TCP port.", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": "string" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "Number or name of the port to access on the container.\nNumber must be in the range 1 to 65535.\nName must be an IANA_SVC_NAME.", + "x-kubernetes-int-or-string": true + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure.\nThe grace period is the duration in seconds after the processes running in the pod are sent\na termination signal and the time when the processes are forcibly halted with a kill signal.\nSet this value longer than the expected cleanup time for your process.\nIf this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this\nvalue overrides the value provided by the pod spec.\nValue must be non-negative integer. The value zero indicates stop immediately via\nthe kill signal (no opportunity to shut down).\nThis is a beta field and requires enabling ProbeTerminationGracePeriod feature gate.\nMinimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": "integer" + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out.\nDefaults to 1 second. Minimum value is 1.\nMore info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": "integer" + } + }, + "type": "object" + }, + "readiness": { + "description": "Probe describes a health check to be performed against a container to determine whether it is\nalive or ready to receive traffic.", + "properties": { + "exec": { + "description": "Exec specifies a command to execute in the container.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the\ncommand is root ('/') in the container's filesystem. The command is simply exec'd, it is\nnot run inside a shell, so traditional shell instructions ('|', etc) won't work. To use\na shell, you need to explicitly call out to that shell.\nExit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded.\nDefaults to 3. Minimum value is 1.", + "format": "int32", + "type": "integer" + }, + "grpc": { + "description": "GRPC specifies a GRPC HealthCheckRequest.", + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "default": "", + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest\n(see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": "string" + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "httpGet": { + "description": "HTTPGet specifies an HTTP GET request to perform.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set\n\"Host\" in httpHeaders instead.", + "type": "string" + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name.\nThis will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": "string" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "Name or number of the port to access on the container.\nNumber must be in the range 1 to 65535.\nName must be an IANA_SVC_NAME.", + "x-kubernetes-int-or-string": true + }, + "scheme": { + "description": "Scheme to use for connecting to the host.\nDefaults to HTTP.", + "type": "string" + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated.\nMore info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": "integer" + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe.\nDefault to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": "integer" + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed.\nDefaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": "integer" + }, + "tcpSocket": { + "description": "TCPSocket specifies a connection to a TCP port.", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": "string" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "Number or name of the port to access on the container.\nNumber must be in the range 1 to 65535.\nName must be an IANA_SVC_NAME.", + "x-kubernetes-int-or-string": true + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure.\nThe grace period is the duration in seconds after the processes running in the pod are sent\na termination signal and the time when the processes are forcibly halted with a kill signal.\nSet this value longer than the expected cleanup time for your process.\nIf this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this\nvalue overrides the value provided by the pod spec.\nValue must be non-negative integer. The value zero indicates stop immediately via\nthe kill signal (no opportunity to shut down).\nThis is a beta field and requires enabling ProbeTerminationGracePeriod feature gate.\nMinimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": "integer" + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out.\nDefaults to 1 second. Minimum value is 1.\nMore info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": "integer" + } + }, + "type": "object" + }, + "startup": { + "description": "Probe describes a health check to be performed against a container to determine whether it is\nalive or ready to receive traffic.", + "properties": { + "exec": { + "description": "Exec specifies a command to execute in the container.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the\ncommand is root ('/') in the container's filesystem. The command is simply exec'd, it is\nnot run inside a shell, so traditional shell instructions ('|', etc) won't work. To use\na shell, you need to explicitly call out to that shell.\nExit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded.\nDefaults to 3. Minimum value is 1.", + "format": "int32", + "type": "integer" + }, + "grpc": { + "description": "GRPC specifies a GRPC HealthCheckRequest.", + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "default": "", + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest\n(see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": "string" + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "httpGet": { + "description": "HTTPGet specifies an HTTP GET request to perform.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set\n\"Host\" in httpHeaders instead.", + "type": "string" + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name.\nThis will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": "string" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "Name or number of the port to access on the container.\nNumber must be in the range 1 to 65535.\nName must be an IANA_SVC_NAME.", + "x-kubernetes-int-or-string": true + }, + "scheme": { + "description": "Scheme to use for connecting to the host.\nDefaults to HTTP.", + "type": "string" + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated.\nMore info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": "integer" + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe.\nDefault to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": "integer" + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed.\nDefaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": "integer" + }, + "tcpSocket": { + "description": "TCPSocket specifies a connection to a TCP port.", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": "string" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "Number or name of the port to access on the container.\nNumber must be in the range 1 to 65535.\nName must be an IANA_SVC_NAME.", + "x-kubernetes-int-or-string": true + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure.\nThe grace period is the duration in seconds after the processes running in the pod are sent\na termination signal and the time when the processes are forcibly halted with a kill signal.\nSet this value longer than the expected cleanup time for your process.\nIf this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this\nvalue overrides the value provided by the pod spec.\nValue must be non-negative integer. The value zero indicates stop immediately via\nthe kill signal (no opportunity to shut down).\nThis is a beta field and requires enabling ProbeTerminationGracePeriod feature gate.\nMinimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": "integer" + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out.\nDefaults to 1 second. Minimum value is 1.\nMore info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "rcon": { + "description": "RCON declares the remote-console protocol used by this game, if any.", + "properties": { + "passwordEnv": { + "description": "PasswordEnv is the environment variable the game container reads\nthe RCON password from (e.g. \"RCON_PASSWORD\" for itzg/minecraft).\nWhen set, the operator injects the resolved password into the game\ncontainer via this env var and mounts the same value for the agent\nsidecar, so the dashboard console can authenticate. Leave empty for\ngames that take their RCON password some other way.", + "type": "string" + }, + "passwordFile": { + "description": "PasswordFile is the path, relative to the game data mount, of a\npassword file the game image manages itself (e.g. \"config/rconpw\"\nfor factorio). When set, the operator does not generate or mount an\nRCON Secret and does not inject PasswordEnv; the agent reads the\npassword from this file inside the data volume on every connection.\nIgnored when PasswordSecretRef is set.", + "maxLength": 255, + "pattern": "^([A-Za-z0-9_][A-Za-z0-9_.-]*/)*[A-Za-z0-9_][A-Za-z0-9_.-]*$", + "type": "string" + }, + "passwordSecretRef": { + "description": "PasswordSecretRef references a Secret+key containing the RCON\npassword. If unset, the operator generates a password and stores\nit in an auto-managed Secret (-rcon, key \"password\").", + "properties": { + "key": { + "minLength": 1, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "port": { + "description": "Port is the port RCON listens on inside the pod (TCP for source,\ntelnet, websocket, satisfactory, and palworld; UDP for battleye).", + "format": "int32", + "type": "integer" + }, + "protocol": { + "default": "source", + "description": "Protocol is the wire protocol the agent speaks to the game's console\nport. Multiple protocols are supported: \"source\" is the Valve/Minecraft\npacket-framed RCON protocol; \"telnet\" is a raw line-based TCP console\n(e.g. 7 Days to Die) \u2014 send a line, get a line back, no framing;\n\"websocket\" is the Rust WebRcon protocol (requires +rcon.web 1);\n\"battleye\" is the BattlEye RCon protocol used by DayZ and Arma \u2014 UDP,\nwith checksum-framed packets and a mandatory client-side keepalive;\n\"satisfactory\" is Satisfactory Dedicated Server's HTTPS function-call\nAPI (POST /api/v1 with a JSON \"function\" body, bearer-token auth\nafter a PasswordLogin call) \u2014 not a socket protocol at all, so Port\nhere is a TCP port carrying HTTPS, not a raw console stream.\n\"palworld\" is Palworld Dedicated Server's REST admin API (plain HTTP,\nnot HTTPS \u2014 GET/POST under /v1/api/..., HTTP Basic auth with\nusername \"admin\" sent on every request, no token or session) \u2014 the\nofficial replacement for Palworld's now-deprecated source RCON, and\nlikewise not a socket protocol, so Port is a TCP port carrying HTTP.\n\"none\" means the game has no usable remote console (see consoleMode:\npty for stdin-driven games instead).", + "enum": [ + "source", + "telnet", + "websocket", + "battleye", + "satisfactory", + "palworld", + "none" + ], + "type": "string" + } + }, + "type": "object" + }, + "resources": { + "description": "Resources are the default compute resources for the game container.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims,\nthat are used by this container.\n\nThis field depends on the\nDynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of\nthe Pod where this field is used. It makes that resource available\ninside a container.", + "type": "string" + }, + "request": { + "description": "Request is the name chosen for a request in the referenced claim.\nIf empty, everything from the claim is made available, otherwise\nonly the result of this request.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "description": "Limits describes the maximum amount of compute resources allowed.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + }, + "requests": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "description": "Requests describes the minimum amount of compute resources required.\nIf Requests is omitted for a container, it defaults to Limits if that is explicitly specified,\notherwise to an implementation-defined value. Requests cannot exceed Limits.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + } + }, + "type": "object" + }, + "security": { + "description": "Security overrides the pod/container security settings for games whose\nimage refuses to run as root or expects a specific uid (e.g. ARK's\nimage requires uid 25000 and cannot initialise Proton as root).\nOmitted means today's behaviour: the image's own default user.", + "properties": { + "fsGroup": { + "description": "FSGroup makes the kubelet chown the mounted volumes to this gid on\nstart, so a non-root game user can write to its PVC. This is what\nreplaces the chown init-container such images normally ship with.", + "format": "int64", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "runAsGroup": { + "description": "RunAsGroup is the gid the GAME container runs as.", + "format": "int64", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "runAsUser": { + "description": "RunAsUser is the uid the GAME container runs as.", + "format": "int64", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + } + }, + "type": "object" + }, + "storage": { + "description": "Storage describes the default persistent storage layout.", + "properties": { + "dataSource": { + "description": "DataSource, when set on a GameServer, seeds the data PVC from an\nexisting CSI VolumeSnapshot the first time the PVC is created. This\nis how volume-snapshot Restores stand up a new server from a\nsnapshot. It is immutable once the PVC binds and has no effect when\nset on a GameTemplate (it is a per-server restore concern).", + "properties": { + "kind": { + "default": "VolumeSnapshot", + "description": "Kind is the data-source object kind. Only \"VolumeSnapshot\" is\nsupported today.", + "enum": [ + "VolumeSnapshot" + ], + "type": "string" + }, + "name": { + "description": "Name is the VolumeSnapshot object name in the GameServer's namespace.", + "maxLength": 253, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": "object" + }, + "extra": { + "description": "Extra declares additional persistent volumes beyond the primary data\nvolume. Use it when a game's persistent state lives in several\ndirectories with no safe common parent \u2014 e.g. 7 Days to Die keeps its\n17GB install in serverfiles/ and its worlds in .local/share/, whose only\nshared parent also holds the entrypoint script, so mounting there would\nshadow it. This same field lives on GameServer.spec.storage (an alias\nof GameStorageSpec): a GameServer that sets a non-empty Extra replaces\nthe template's list wholesale, mirroring how Size/StorageClassName\nalready override the template on that field \u2014 it does not merge\nentry-by-entry.", + "items": { + "description": "ExtraVolumeSpec declares one additional persistent volume beyond a\nGameTemplate's primary data volume (see GameStorageSpec.Extra). The\noperator provisions one PVC per entry using the same mechanism as the\nprimary data PVC (create-if-absent, ReadWriteOnce, owned by the\nGameServer) under its own \"-extra-\" PVC name, and mounts it\ndirectly at MountPath on the game container \u2014 never nested under the\nprimary Storage.MountPath, since these exist precisely for directories\nthat share no safe common parent with it.", + "properties": { + "mountPath": { + "description": "MountPath is the absolute in-container path this volume is mounted at.", + "maxLength": 255, + "pattern": "^/.*", + "type": "string" + }, + "name": { + "description": "Name identifies the volume and forms part of its PVC name. Must be a\nDNS-1123 label.", + "maxLength": 40, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "type": "string" + }, + "size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "Size is the requested capacity (e.g. \"40Gi\").", + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + } + }, + "required": [ + "mountPath", + "name", + "size" + ], + "type": "object" + }, + "maxItems": 4, + "type": "array" + }, + "mountPath": { + "default": "/data", + "description": "MountPath is where the game persistent volume is mounted inside\nthe game container (e.g. \"/data\").", + "type": "string" + }, + "size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "Size is the default PVC size (e.g. \"10Gi\").", + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "storageClassName": { + "description": "StorageClassName, when set, pins the PVC to a specific StorageClass.", + "type": "string" + } + }, + "type": "object" + }, + "version": { + "description": "Version is the template revision (e.g. \"1.0.0\"). Bump when changing\ndefaults in ways that existing GameServers should opt into.", + "minLength": 1, + "type": "string" + }, + "versions": { + "description": "Versions is an optional catalog of selectable game versions surfaced\nin the Create Server wizard. Each entry maps a user choice to a\nconcrete image (and optional per-version env / mod loader). When empty\nthere is no version choice: a server runs spec.image (override) or this\ntemplate's Image, exactly as before. At most one entry should set\ndefault=true; otherwise the first entry is the wizard's default.", + "items": { + "description": "GameVersion is one selectable entry in a template's version catalog.\nSelecting it (via GameServer.spec.version) pins the container image,\nappends this entry's env, and \u2014 when Loader names a capabilities.mods\nloader \u2014 provisions and mounts that loader's per-(version+loader) mod\nvolume.", + "properties": { + "default": { + "description": "Default marks the wizard's pre-selected entry. At most one entry should\nset it; if none (or several) do, the first entry is the default.", + "type": "boolean" + }, + "displayName": { + "description": "DisplayName labels the entry in the version picker, e.g.\n\"1.21.4 (Paper)\".", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "env": { + "description": "Env is appended when this version is selected (e.g. TYPE=PAPER,\nVERSION=1.21.4 for itzg). It is applied after the template's Env and\nbefore GameServer.spec.env, so an explicit user override still wins.", + "items": { + "description": "EnvVar represents an environment variable present in a Container.", + "properties": { + "name": { + "description": "Name of the environment variable.\nMay consist of any printable ASCII characters except '='.", + "type": "string" + }, + "value": { + "description": "Variable references $(VAR_NAME) are expanded\nusing the previously defined environment variables in the container and\nany service environment variables. If a variable cannot be resolved,\nthe reference in the input string will be unchanged. Double $$ are reduced\nto a single $, which allows for escaping the $(VAR_NAME) syntax: i.e.\n\"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\".\nEscaped references will never be expanded, regardless of whether the variable\nexists or not.\nDefaults to \"\".", + "type": "string" + }, + "valueFrom": { + "description": "Source for the environment variable's value. Cannot be used if value is not empty.", + "properties": { + "configMapKeyRef": { + "description": "Selects a key of a ConfigMap.", + "properties": { + "key": { + "description": "The key to select.", + "type": "string" + }, + "name": { + "default": "", + "description": "Name of the referent.\nThis field is effectively required, but due to backwards compatibility is\nallowed to be empty. Instances of this type with an empty value here are\nalmost certainly wrong.\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "Specify whether the ConfigMap or its key must be defined", + "type": "boolean" + } + }, + "required": [ + "key" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "fieldRef": { + "description": "Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`,\nspec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": "string" + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "fileKeyRef": { + "description": "FileKeyRef selects a key of the env file.\nRequires the EnvFiles feature gate to be enabled.", + "properties": { + "key": { + "description": "The key within the env file. An invalid key will prevent the pod from starting.\nThe keys defined within a source may consist of any printable ASCII characters except '='.\nDuring Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters.", + "type": "string" + }, + "optional": { + "default": false, + "description": "Specify whether the file or its key must be defined. If the file or key\ndoes not exist, then the env var is not published.\nIf optional is set to true and the specified key does not exist,\nthe environment variable will not be set in the Pod's containers.\n\nIf optional is set to false and the specified key does not exist,\nan error will be returned during Pod creation.", + "type": "boolean" + }, + "path": { + "description": "The path within the volume from which to select the file.\nMust be relative and may not contain the '..' path or start with '..'.", + "type": "string" + }, + "volumeName": { + "description": "The name of the volume mount containing the env file.", + "type": "string" + } + }, + "required": [ + "key", + "path", + "volumeName" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "resourceFieldRef": { + "description": "Selects a resource of the container: only resources limits and requests\n(limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": "string" + }, + "divisor": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "Specifies the output format of the exposed resources, defaults to \"1\"", + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "secretKeyRef": { + "description": "Selects a key of a secret in the pod's namespace", + "properties": { + "key": { + "description": "The key of the secret to select from. Must be a valid secret key.", + "type": "string" + }, + "name": { + "default": "", + "description": "Name of the referent.\nThis field is effectively required, but due to backwards compatibility is\nallowed to be empty. Instances of this type with an empty value here are\nalmost certainly wrong.\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "Specify whether the Secret or its key must be defined", + "type": "boolean" + } + }, + "required": [ + "key" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + } + }, + "type": "object" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + }, + "gameVersion": { + "description": "GameVersion is the upstream game-version token the dashboard passes to\nan external mod registry to filter results to this version (e.g.\nModrinth's \"game_versions\" facet), e.g. \"1.21.4\". It is distinct from\nID (a Gameplane selector like \"1.21.4-paper\" that's too lossy to parse\nreliably). Optional; when unset the registry search sends no version\nfacet and returns mods for all versions. Ignored by registries with no\nversion dimension (e.g. Thunderstore) and when the template declares no\nmods.registry.", + "maxLength": 32, + "type": "string" + }, + "id": { + "description": "ID is the stable selector stored in GameServer.spec.version. It is\nalso folded into the per-version mod volume/PVC names, so keep it short\nand DNS-ish (dots and hyphens allowed; sanitized to a DNS label for\nvolume names). E.g. \"1.21.4-paper\", \"tmodloader-latest\".", + "maxLength": 40, + "pattern": "^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$", + "type": "string" + }, + "image": { + "description": "Image is the full container reference for this version. For\nenv-versioned games (e.g. itzg/minecraft-server, which picks software\nvia TYPE/VERSION env) this is usually one pinned image shared across\nentries, differentiated only by Loader/Env. For tag-versioned games\n(e.g. Terraria) each entry pins a distinct tag.", + "minLength": 1, + "type": "string" + }, + "loader": { + "description": "Loader is the mod-loader / server-type key (e.g. \"paper\", \"forge\",\n\"fabric\", \"vanilla\", \"bepinex\", \"tmodloader\"). It keys into\ncapabilities.mods.loaders to select this combo's mod volume. Empty\nmeans this version has no loader dimension (mods, if any, live on the\nshared data volume).", + "maxLength": 40, + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", + "type": "string" + } + }, + "required": [ + "displayName", + "id", + "image" + ], + "type": "object" + }, + "maxItems": 64, + "type": "array" + } + }, + "required": [ + "displayName", + "game", + "image", + "version" + ], + "type": "object", + "x-kubernetes-validations": [ + { + "message": "consoleMode 'rcon' requires spec.rcon with a protocol other than 'none'", + "rule": "!has(self.consoleMode) || self.consoleMode != 'rcon' || (has(self.rcon) && (!has(self.rcon.protocol) || self.rcon.protocol != 'none'))" + }, + { + "message": "capabilities.mods.loaders requires a non-empty spec.versions catalog (a mod loader is selected by versions[].loader)", + "rule": "!has(self.capabilities) || !has(self.capabilities.mods) || !has(self.capabilities.mods.loaders) || size(self.capabilities.mods.loaders) == 0 || (has(self.versions) && size(self.versions) > 0)" + } + ] + }, + "status": { + "description": "GameTemplateStatus is intentionally minimal \u2014 templates are mostly\nstatic configuration. The operator populates ObservedGeneration so\ncontrollers reading templates can detect spec updates.", + "properties": { + "inUseCount": { + "description": "InUseCount is the number of GameServers currently referencing\nthis template.", + "format": "int32", + "type": "integer" + }, + "observedGeneration": { + "format": "int64", + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Gameplane GameTemplate (module template.yaml)" +} diff --git a/.schema/module.schema.json b/.schema/module.schema.json new file mode 100644 index 0000000..04a8213 --- /dev/null +++ b/.schema/module.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Gameplane module manifest (module.yaml)", + "description": "The bundle metadata layer of a Gameplane module. See docs/module-authoring.md in the Gameplane repo for the authoritative format.", + "type": "object", + "additionalProperties": false, + "required": ["apiVersion", "name", "displayName", "version", "game", "summary"], + "properties": { + "apiVersion": { + "const": "gameplane.local/module/v1" + }, + "name": { + "type": "string", + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "maxLength": 63, + "description": "DNS-1123 label; must match the module directory name." + }, + "displayName": { + "type": "string", + "minLength": 1, + "description": "Human-readable name shown on the catalog card." + }, + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$", + "description": "Semver; must match the OCI tag the bundle is pushed under." + }, + "game": { + "type": "string", + "minLength": 1, + "description": "Free-form game family identifier (e.g. minecraft-java)." + }, + "categories": { + "type": "array", + "items": { "type": "string" }, + "description": "Catalog groupings; a module may belong to several." + }, + "summary": { + "type": "string", + "minLength": 1, + "description": "One-line description for the catalog card." + }, + "homepage": { + "type": "string", + "format": "uri", + "description": "Optional project/game homepage URL." + }, + "license": { + "type": "string", + "description": "Optional SPDX license identifier." + }, + "gameplaneMinVersion": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?$", + "description": "Optional minimum operator version; install is refused on older operators." + }, + "icon": { + "type": "string", + "description": "Optional filename of the icon layer (e.g. icon.png)." + } + } +} diff --git a/7-days-to-die/module.yaml b/7-days-to-die/module.yaml index 4d2222a..dcb3ba7 100644 --- a/7-days-to-die/module.yaml +++ b/7-days-to-die/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: 7-days-to-die displayName: 7 Days to Die diff --git a/7-days-to-die/template.yaml b/7-days-to-die/template.yaml index db9d7e1..58c9d19 100644 --- a/7-days-to-die/template.yaml +++ b/7-days-to-die/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for 7 Days to Die (dedicated server). # # Backed by vinanrra/7dtd-server (https://github.com/vinanrra/Docker-7DaysToDie), diff --git a/ark-survival-ascended/module.yaml b/ark-survival-ascended/module.yaml index be30d55..64db089 100644 --- a/ark-survival-ascended/module.yaml +++ b/ark-survival-ascended/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: ark-survival-ascended displayName: "ARK: Survival Ascended" diff --git a/ark-survival-ascended/template.yaml b/ark-survival-ascended/template.yaml index cf798b0..69ca9fd 100644 --- a/ark-survival-ascended/template.yaml +++ b/ark-survival-ascended/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for ARK: Survival Ascended (dedicated server). # # Backed by mschnitzer/asa-linux-server diff --git a/cs2/module.yaml b/cs2/module.yaml index 0dc2a58..1ec4011 100644 --- a/cs2/module.yaml +++ b/cs2/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: cs2 displayName: Counter-Strike 2 diff --git a/cs2/template.yaml b/cs2/template.yaml index 83d3e0c..0d96c1c 100644 --- a/cs2/template.yaml +++ b/cs2/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for Counter-Strike 2 (dedicated server). # # Backed by joedwards32/CS2 (cm2network/cs2 is a downstream mirror of the diff --git a/dayz/module.yaml b/dayz/module.yaml index bdf9255..8a677d3 100644 --- a/dayz/module.yaml +++ b/dayz/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: dayz displayName: DayZ diff --git a/dayz/template.yaml b/dayz/template.yaml index 2d2aed3..d3b8684 100644 --- a/dayz/template.yaml +++ b/dayz/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for DayZ (dedicated server). # # Backed by GodBleak/ServerZ (https://github.com/GodBleak/ServerZ), pulled diff --git a/dont-starve-together/module.yaml b/dont-starve-together/module.yaml index 1ff3a26..48d9480 100644 --- a/dont-starve-together/module.yaml +++ b/dont-starve-together/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: dont-starve-together displayName: Don't Starve Together diff --git a/dont-starve-together/template.yaml b/dont-starve-together/template.yaml index d99d771..09dd421 100644 --- a/dont-starve-together/template.yaml +++ b/dont-starve-together/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json apiVersion: gameplane.local/v1alpha1 kind: GameTemplate metadata: diff --git a/enshrouded/module.yaml b/enshrouded/module.yaml index 86b129a..55c3401 100644 --- a/enshrouded/module.yaml +++ b/enshrouded/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: enshrouded displayName: Enshrouded diff --git a/enshrouded/template.yaml b/enshrouded/template.yaml index f433bc6..76b1a07 100644 --- a/enshrouded/template.yaml +++ b/enshrouded/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json apiVersion: gameplane.local/v1alpha1 kind: GameTemplate metadata: diff --git a/factorio/module.yaml b/factorio/module.yaml index 27a700d..40082f1 100644 --- a/factorio/module.yaml +++ b/factorio/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: factorio displayName: Factorio (Headless) diff --git a/factorio/template.yaml b/factorio/template.yaml index 9b44589..3fe0f49 100644 --- a/factorio/template.yaml +++ b/factorio/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for Factorio (headless server). # # Backed by the official-community factoriotools/factorio image. The game diff --git a/garrys-mod/module.yaml b/garrys-mod/module.yaml index 3ba4e85..520e5b3 100644 --- a/garrys-mod/module.yaml +++ b/garrys-mod/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: garrys-mod displayName: Garry's Mod diff --git a/garrys-mod/template.yaml b/garrys-mod/template.yaml index e69ae1d..d254ed8 100644 --- a/garrys-mod/template.yaml +++ b/garrys-mod/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for Garry's Mod (dedicated server). # # Backed by ceifa/garrysmod (https://github.com/ceifa/garrysmod-docker). The diff --git a/minecraft-java/module.yaml b/minecraft-java/module.yaml index 464221e..4113054 100644 --- a/minecraft-java/module.yaml +++ b/minecraft-java/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: minecraft-java displayName: Minecraft (Java Edition) diff --git a/minecraft-java/template.yaml b/minecraft-java/template.yaml index c46b0dc..c6793aa 100644 --- a/minecraft-java/template.yaml +++ b/minecraft-java/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for Minecraft: Java Edition. # # Backed by the community itzg/minecraft-server image, which downloads the diff --git a/palworld/module.yaml b/palworld/module.yaml index 622eb9e..207f769 100644 --- a/palworld/module.yaml +++ b/palworld/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: palworld displayName: Palworld (Dedicated) diff --git a/palworld/template.yaml b/palworld/template.yaml index 92a716c..7cded6d 100644 --- a/palworld/template.yaml +++ b/palworld/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for Palworld (dedicated server). # # Backed by the community thijsvanloef/palworld-server-docker image. The diff --git a/project-zomboid/module.yaml b/project-zomboid/module.yaml index 78f5b58..22ac726 100644 --- a/project-zomboid/module.yaml +++ b/project-zomboid/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: project-zomboid displayName: Project Zomboid diff --git a/project-zomboid/template.yaml b/project-zomboid/template.yaml index 84e387d..e33b5cc 100644 --- a/project-zomboid/template.yaml +++ b/project-zomboid/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for Project Zomboid (dedicated server). # # Backed by sknnr/zomboid-dedicated-server (https://github.com/jsknnr/project- diff --git a/rust/module.yaml b/rust/module.yaml index ad500e8..649b70b 100644 --- a/rust/module.yaml +++ b/rust/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: rust displayName: Rust diff --git a/rust/template.yaml b/rust/template.yaml index 5cd84be..e95bcf4 100644 --- a/rust/template.yaml +++ b/rust/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for Rust (dedicated server). # # Backed by didstopia/rust-server. Rust's RCON is WebSocket-only — Facepunch's diff --git a/satisfactory/module.yaml b/satisfactory/module.yaml index 148f391..7d09e18 100644 --- a/satisfactory/module.yaml +++ b/satisfactory/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: satisfactory displayName: Satisfactory (Dedicated) diff --git a/satisfactory/template.yaml b/satisfactory/template.yaml index 3e07f05..1ea766a 100644 --- a/satisfactory/template.yaml +++ b/satisfactory/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for Satisfactory (dedicated server). # # Backed by the community wolveix/satisfactory-server image, which installs diff --git a/terraria/module.yaml b/terraria/module.yaml index cc764ba..592799b 100644 --- a/terraria/module.yaml +++ b/terraria/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: terraria displayName: Terraria diff --git a/terraria/template.yaml b/terraria/template.yaml index 1878051..71726f7 100644 --- a/terraria/template.yaml +++ b/terraria/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for Terraria (vanilla + tModLoader). # # Backed by passivelemon/terraria-docker. Vanilla vs tModLoader (and the diff --git a/v-rising/module.yaml b/v-rising/module.yaml index ea9775f..5814e90 100644 --- a/v-rising/module.yaml +++ b/v-rising/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: v-rising displayName: V Rising diff --git a/v-rising/template.yaml b/v-rising/template.yaml index 5e81eb5..eec7273 100644 --- a/v-rising/template.yaml +++ b/v-rising/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for V Rising (dedicated server). # # Backed by trueosiris/vrising (Ubuntu + Wine). The image documents two diff --git a/valheim/module.yaml b/valheim/module.yaml index 7dba4f1..4db9a3f 100644 --- a/valheim/module.yaml +++ b/valheim/module.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/module.schema.json apiVersion: gameplane.local/module/v1 name: valheim displayName: Valheim (Dedicated) diff --git a/valheim/template.yaml b/valheim/template.yaml index 5886e6d..1b42818 100644 --- a/valheim/template.yaml +++ b/valheim/template.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../.schema/gametemplate.schema.json # Gameplane GameTemplate for Valheim (dedicated server). # # Backed by lloesche/valheim-server, which installs the game via SteamCMD on