diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a95d007..5c4caf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,9 @@ jobs: - cockpit - grounds-service - agones-fleet + - grounds-velocity + - plugin-velocity-jar + - grounds-gamemode steps: - name: 📥 Checkout code uses: actions/checkout@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9624e58..da36f66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,6 +27,9 @@ jobs: - cockpit - grounds-service - agones-fleet + - grounds-velocity + - plugin-velocity-jar + - grounds-gamemode env: MATCHES_REF: ${{ startsWith(github.ref, format('refs/tags/{0}-v', matrix.chart)) }} steps: diff --git a/.gitignore b/.gitignore index 03a15b1..1b9732f 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ build/ *.tar *.tar.gz +# AI +.codex +.cursor diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7d252a8..91d617c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -3,5 +3,8 @@ "charts/grounds-api": "0.2.0", "charts/cockpit": "0.1.1", "charts/grounds-service": "0.1.0", - "charts/agones-fleet": "0.2.0" + "charts/agones-fleet": "0.2.0", + "charts/grounds-velocity": "0.0.1", + "charts/plugin-velocity-jar": "0.0.1", + "charts/grounds-gamemode": "0.0.1" } diff --git a/charts/grounds-gamemode/Chart.lock b/charts/grounds-gamemode/Chart.lock new file mode 100644 index 0000000..428ee53 --- /dev/null +++ b/charts/grounds-gamemode/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: common + repository: oci://ghcr.io/groundsgg/charts + version: 0.2.0 +digest: sha256:cc3072b8b38019dbcb82c3adde2774f0ade4c29ea47b961c49936a99d44556d2 +generated: "2026-05-03T20:27:15.738113+02:00" diff --git a/charts/grounds-gamemode/Chart.yaml b/charts/grounds-gamemode/Chart.yaml new file mode 100644 index 0000000..f4c86e4 --- /dev/null +++ b/charts/grounds-gamemode/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: grounds-gamemode +description: Minecraft game-server (Paper / Minestom) — Deployment by default, Agones Fleet when scaling +type: application +version: 0.1.0 + +dependencies: + - name: common + version: "0.2.0" + repository: "oci://ghcr.io/groundsgg/charts" diff --git a/charts/grounds-gamemode/templates/_helpers.tpl b/charts/grounds-gamemode/templates/_helpers.tpl new file mode 100644 index 0000000..eb8b57a --- /dev/null +++ b/charts/grounds-gamemode/templates/_helpers.tpl @@ -0,0 +1,31 @@ +{{/* +Returns the env block for the game-server container, picking the right +velocity-secret env-var name based on `.Values.kind`. +*/}} +{{- define "grounds-gamemode.env" -}} +{{- $secretEnvName := "" -}} +{{- if eq .Values.kind "lobby" -}} +{{- $secretEnvName = "GROUNDS_LOBBY_VELOCITY_SECRET" -}} +{{- else -}} +{{- $secretEnvName = "PAPER_VELOCITY_SECRET" -}} +{{- end -}} +- name: {{ $secretEnvName }} + valueFrom: + secretKeyRef: + name: {{ .Values.forwardingSecret.name }} + key: {{ .Values.forwardingSecret.key }} +{{- with .Values.extraEnv }} +{{ toYaml . }} +{{- end }} +{{- end -}} + +{{/* +Resolves the fully-qualified image reference. +*/}} +{{- define "grounds-gamemode.image" -}} +{{- $repo := .Values.image.repository -}} +{{- if .Values.image.registry -}} +{{- $repo = printf "%s/%s" .Values.image.registry .Values.image.repository -}} +{{- end -}} +{{ printf "%s:%s" $repo .Values.image.tag }} +{{- end -}} diff --git a/charts/grounds-gamemode/templates/deployment.yaml b/charts/grounds-gamemode/templates/deployment.yaml new file mode 100644 index 0000000..505fe13 --- /dev/null +++ b/charts/grounds-gamemode/templates/deployment.yaml @@ -0,0 +1,54 @@ +{{- if not .Values.agones.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + labels: + {{- include "common.labels" . | nindent 4 }} + grounds/component: gamemode + grounds/server-type: {{ .Values.kind }} + {{- with .Values.global.commonLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.global.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + {{- include "common.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "common.podLabels" . | nindent 8 }} + grounds/component: gamemode + grounds/server-type: {{ .Values.kind }} + {{- with .Values.global.commonLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.global.commonAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + containers: + - name: {{ .Release.Name }} + image: {{ include "grounds-gamemode.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: minecraft + containerPort: {{ .Values.containerPort }} + protocol: TCP + env: + {{- include "grounds-gamemode.env" . | nindent 12 }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} +{{- end }} diff --git a/charts/grounds-gamemode/templates/fleet.yaml b/charts/grounds-gamemode/templates/fleet.yaml new file mode 100644 index 0000000..e1363d0 --- /dev/null +++ b/charts/grounds-gamemode/templates/fleet.yaml @@ -0,0 +1,61 @@ +{{- if .Values.agones.enabled }} +apiVersion: agones.dev/v1 +kind: Fleet +metadata: + name: {{ .Release.Name }} + labels: + {{- include "common.labels" . | nindent 4 }} + grounds/component: gamemode + grounds/server-type: {{ .Values.kind }} + {{- with .Values.global.commonLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.global.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.agones.fleet.minReplicas }} + template: + metadata: + labels: + grounds/component: gamemode + grounds/server-type: {{ .Values.kind }} + spec: + ports: + - name: minecraft + portPolicy: Dynamic + containerPort: {{ .Values.containerPort }} + protocol: TCP + container: {{ .Release.Name }} + sdkServer: + logLevel: Info + httpPort: 9358 + health: + disabled: true + template: + metadata: + labels: + {{- include "common.podLabels" . | nindent 12 }} + grounds/component: gamemode + grounds/server-type: {{ .Values.kind }} + spec: + containers: + - name: {{ .Release.Name }} + image: {{ include "grounds-gamemode.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.command }} + command: + {{- toYaml . | nindent 16 }} + {{- end }} + ports: + - name: minecraft + containerPort: {{ .Values.containerPort }} + protocol: TCP + env: + {{- include "grounds-gamemode.env" . | nindent 16 }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 16 }} + {{- end }} +{{- end }} diff --git a/charts/grounds-gamemode/templates/service.yaml b/charts/grounds-gamemode/templates/service.yaml new file mode 100644 index 0000000..248771c --- /dev/null +++ b/charts/grounds-gamemode/templates/service.yaml @@ -0,0 +1,25 @@ +{{- if not .Values.agones.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + labels: + {{- include "common.labels" . | nindent 4 }} + grounds/server-type: {{ .Values.kind }} + {{- with .Values.global.commonLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.global.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.containerPort }} + targetPort: minecraft + protocol: TCP + name: minecraft + selector: + {{- include "common.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/grounds-gamemode/values.yaml b/charts/grounds-gamemode/values.yaml new file mode 100644 index 0000000..587faee --- /dev/null +++ b/charts/grounds-gamemode/values.yaml @@ -0,0 +1,68 @@ +# Default values for grounds-gamemode. +# +# A grounds-gamemode release is one Minecraft game-server kind. The +# chart picks between two backings: +# - `agones.enabled: true` → Agones Fleet (autoscaled, dynamic ports) +# - `agones.enabled: false` → Deployment + Service (fixed port 25565) +# +# Used in the platform-test environment for both: +# - minestom-lobby (kind: lobby, Deployment, single replica) +# - paper-game (kind: paper, Agones Fleet, scales 0..N) +# +# kind drives which Velocity-secret env-var the container expects: +# - "lobby" → GROUNDS_LOBBY_VELOCITY_SECRET +# - "paper" → PAPER_VELOCITY_SECRET +# - "match" → PAPER_VELOCITY_SECRET +# - "game" → PAPER_VELOCITY_SECRET + +global: + commonLabels: {} + commonAnnotations: {} + +# Logical kind of the game-server. See note above. +kind: "paper" + +image: + registry: "ghcr.io" + repository: "groundsgg/paper-game" + tag: "latest" + pullPolicy: IfNotPresent + +# Agones Fleet mode — set agones.enabled=true to switch from Deployment +# to a Fleet. Bundle.yaml's `helm.agones.fleet` map maps directly here. +# This chart currently uses only `minReplicas` as the Fleet `.spec.replicas` +# value; it does not define a FleetAutoscaler. +agones: + enabled: false + fleet: + minReplicas: 0 + maxReplicas: 2 + +# Deployment-mode config (ignored when agones.enabled). +replicas: 1 + +# velocity-forwarding-secret is provisioned out-of-band; the chart +# only references it. Same convention as grounds-velocity. +forwardingSecret: + name: "velocity-forwarding-secret" + key: "secret" + +# Container port. Minecraft default 25565. +containerPort: 25565 + +# Optional command override. +command: [] + +# extraEnv merges into env after the kind-specific velocity-secret. +extraEnv: [] + +resources: + requests: + cpu: "500m" + memory: "2Gi" + limits: + cpu: "2" + memory: "4Gi" + +service: + type: ClusterIP diff --git a/charts/grounds-velocity/Chart.lock b/charts/grounds-velocity/Chart.lock new file mode 100644 index 0000000..6ba69c7 --- /dev/null +++ b/charts/grounds-velocity/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: common + repository: oci://ghcr.io/groundsgg/charts + version: 0.2.0 +digest: sha256:cc3072b8b38019dbcb82c3adde2774f0ade4c29ea47b961c49936a99d44556d2 +generated: "2026-05-03T20:26:58.10509+02:00" diff --git a/charts/grounds-velocity/Chart.yaml b/charts/grounds-velocity/Chart.yaml new file mode 100644 index 0000000..58a59d7 --- /dev/null +++ b/charts/grounds-velocity/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: grounds-velocity +description: Velocity Minecraft proxy with per-plugin JAR fetching for the platform-test environment +type: application +version: 0.1.0 + +dependencies: + - name: common + version: "0.2.0" + repository: "oci://ghcr.io/groundsgg/charts" diff --git a/charts/grounds-velocity/templates/deployment.yaml b/charts/grounds-velocity/templates/deployment.yaml new file mode 100644 index 0000000..7c9427f --- /dev/null +++ b/charts/grounds-velocity/templates/deployment.yaml @@ -0,0 +1,83 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + labels: + {{- include "common.labels" . | nindent 4 }} + grounds/component: velocity + {{- with .Values.global.commonLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.global.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + {{- include "common.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "common.podLabels" . | nindent 8 }} + grounds/component: velocity + {{- with .Values.global.commonLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.global.commonAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- $imageRepo := .Values.image.repository }} + {{- if .Values.image.registry }} + {{- $imageRepo = printf "%s/%s" .Values.image.registry .Values.image.repository }} + {{- end }} + {{- with .Values.plugins }} + initContainers: + {{- range $plugin := . }} + - name: fetch-{{ $plugin }} + image: {{ $.Values.fetcher.image | quote }} + imagePullPolicy: IfNotPresent + command: + - sh + - -c + - >- + curl --fail --silent --show-error + --max-time {{ $.Values.fetcher.timeoutSeconds }} + --retry 5 --retry-delay 2 --retry-connrefused + -o /plugins/{{ $plugin }}.jar + http://{{ $plugin }}:8080/plugin.jar + volumeMounts: + - name: plugins + mountPath: /plugins + {{- end }} + {{- end }} + containers: + - name: velocity + image: "{{ $imageRepo }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: minecraft + containerPort: {{ .Values.ports.minecraft }} + protocol: TCP + env: + - name: VELOCITY_FORWARDING_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.forwardingSecret.name }} + key: {{ .Values.forwardingSecret.key }} + {{- with .Values.env }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: plugins + mountPath: /app/plugins + volumes: + - name: plugins + emptyDir: {} diff --git a/charts/grounds-velocity/templates/service.yaml b/charts/grounds-velocity/templates/service.yaml new file mode 100644 index 0000000..b633a53 --- /dev/null +++ b/charts/grounds-velocity/templates/service.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + labels: + {{- include "common.labels" . | nindent 4 }} + {{- with .Values.global.commonLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.global.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + {{- with .Values.service.externalTrafficPolicy }} + externalTrafficPolicy: {{ . }} + {{- end }} + ports: + - port: {{ .Values.ports.minecraft }} + targetPort: minecraft + protocol: TCP + name: minecraft + selector: + {{- include "common.selectorLabels" . | nindent 4 }} diff --git a/charts/grounds-velocity/values.yaml b/charts/grounds-velocity/values.yaml new file mode 100644 index 0000000..a8efd96 --- /dev/null +++ b/charts/grounds-velocity/values.yaml @@ -0,0 +1,58 @@ +# Default values for grounds-velocity. +# +# Per-engineer Velocity proxy. Each plugin in `.plugins[]` corresponds +# to a sibling `plugin-velocity-jar` Helm release in the same namespace +# whose Service serves the JAR over HTTP at /plugin.jar. At pod start +# Velocity runs one init-container per entry to fetch each JAR into the +# emptyDir mounted at /app/plugins. + +global: + commonLabels: {} + commonAnnotations: {} + +replicas: 1 + +image: + registry: "ghcr.io" + repository: "groundsgg/velocity" + tag: "latest" + pullPolicy: IfNotPresent + +# Each entry is the Helm release name of a sibling plugin-velocity-jar +# release. Velocity fetches `http://:8080/plugin.jar` and writes +# it to `/app/plugins/.jar`. Order is preserved. +plugins: [] +# Example: +# plugins: +# - plugin-social +# - plugin-chat + +# Image used for the per-plugin fetch init-containers. +fetcher: + image: "curlimages/curl:8.18.0" + timeoutSeconds: 60 + +resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "2" + memory: "2Gi" + +# Velocity exposes the player port (modern Minecraft proxy). +ports: + minecraft: 25577 + +service: + type: ClusterIP + externalTrafficPolicy: "" + +# velocity-forwarding-secret is provisioned out-of-band (Pulumi/sealed +# secret) so all proxies + game-servers in the namespace share one +# secret. The chart only references it. +forwardingSecret: + name: "velocity-forwarding-secret" + key: "secret" + +env: [] diff --git a/charts/plugin-velocity-jar/Chart.lock b/charts/plugin-velocity-jar/Chart.lock new file mode 100644 index 0000000..173d78f --- /dev/null +++ b/charts/plugin-velocity-jar/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: common + repository: oci://ghcr.io/groundsgg/charts + version: 0.2.0 +digest: sha256:cc3072b8b38019dbcb82c3adde2774f0ade4c29ea47b961c49936a99d44556d2 +generated: "2026-05-03T20:26:40.440813+02:00" diff --git a/charts/plugin-velocity-jar/Chart.yaml b/charts/plugin-velocity-jar/Chart.yaml new file mode 100644 index 0000000..e5f3ebf --- /dev/null +++ b/charts/plugin-velocity-jar/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: plugin-velocity-jar +description: Hosts a Velocity-plugin JAR so the grounds-velocity proxy can fetch it at startup +type: application +version: 0.1.0 + +dependencies: + - name: common + version: "0.2.0" + repository: "oci://ghcr.io/groundsgg/charts" diff --git a/charts/plugin-velocity-jar/templates/deployment.yaml b/charts/plugin-velocity-jar/templates/deployment.yaml new file mode 100644 index 0000000..6493091 --- /dev/null +++ b/charts/plugin-velocity-jar/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + labels: + {{- include "common.labels" . | nindent 4 }} + grounds/component: plugin-velocity-jar + {{- with .Values.global.commonLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.global.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: 1 + selector: + matchLabels: + {{- include "common.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "common.podLabels" . | nindent 8 }} + grounds/component: plugin-velocity-jar + {{- with .Values.global.commonLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.global.commonAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- $imageRepo := .Values.image.repository }} + {{- if .Values.image.registry }} + {{- $imageRepo = printf "%s/%s" .Values.image.registry .Values.image.repository }} + {{- end }} + initContainers: + - name: copy-jar + image: "{{ $imageRepo }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - sh + - -c + - 'cp "{{ .Values.jarPath }}" /shared/plugin.jar && ls -la /shared/plugin.jar' + volumeMounts: + - name: shared + mountPath: /shared + containers: + - name: httpd + image: {{ .Values.httpd.image | quote }} + imagePullPolicy: IfNotPresent + command: ["httpd", "-f", "-p", "{{ .Values.httpd.port }}", "-h", "/shared"] + ports: + - name: http + containerPort: {{ .Values.httpd.port }} + protocol: TCP + readinessProbe: + httpGet: + path: /plugin.jar + port: http + initialDelaySeconds: 1 + periodSeconds: 5 + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: shared + mountPath: /shared + volumes: + - name: shared + emptyDir: {} diff --git a/charts/plugin-velocity-jar/templates/service.yaml b/charts/plugin-velocity-jar/templates/service.yaml new file mode 100644 index 0000000..08a5ead --- /dev/null +++ b/charts/plugin-velocity-jar/templates/service.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + labels: + {{- include "common.labels" . | nindent 4 }} + {{- with .Values.global.commonLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.global.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "common.selectorLabels" . | nindent 4 }} diff --git a/charts/plugin-velocity-jar/values.yaml b/charts/plugin-velocity-jar/values.yaml new file mode 100644 index 0000000..d309b51 --- /dev/null +++ b/charts/plugin-velocity-jar/values.yaml @@ -0,0 +1,53 @@ +# Default values for plugin-velocity-jar. +# +# A plugin-velocity-jar release runs a tiny pod that exposes a single +# Velocity-plugin JAR over HTTP at `/plugin.jar`. The grounds-velocity +# proxy fetches it via init-containers at startup. +# +# Pod layout: +# initContainer "copy-jar" +# image: # carries the JAR at jarPath +# command: cp /shared/plugin.jar +# container "httpd" +# image: busybox httpd -f -h /shared -p 8080 +# mounts /shared (emptyDir, RW so DevSpace can sync new JARs) +# +# This indirection means the plugin image only needs to ship the JAR +# at a known path — no httpd or entrypoint requirements. The DevSpace +# `jar-sync-pod-restart` workflow targets this pod's /shared/plugin.jar +# directly: sync a freshly-built JAR there, then `kubectl rollout +# restart deployment/` so the velocity pod's init-containers +# re-fetch the updated JARs. + +global: + commonLabels: {} + commonAnnotations: {} + +# The plugin-image: must contain the JAR at `jarPath`. Entrypoint is +# irrelevant — we override it to `cp` in the init-container. +image: + registry: "" + repository: "plugin-velocity-jar" + tag: "latest" + pullPolicy: IfNotPresent + +# Where the JAR lives inside the plugin-image. +jarPath: "/jar/plugin.jar" + +# httpd image used to serve the JAR. Override only if you need a +# pinned mirror. +httpd: + image: "busybox:1.37" + port: 8080 + +resources: + requests: + cpu: "10m" + memory: "16Mi" + limits: + cpu: "50m" + memory: "32Mi" + +service: + type: ClusterIP + port: 8080 diff --git a/release-please-config.json b/release-please-config.json index 9473f5f..eedf2e6 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -16,6 +16,15 @@ }, "charts/agones-fleet": { "release-type": "helm" + }, + "charts/grounds-velocity": { + "release-type": "helm" + }, + "charts/plugin-velocity-jar": { + "release-type": "helm" + }, + "charts/grounds-gamemode": { + "release-type": "helm" } } }