diff --git a/.github/workflows/publish-chart.yaml b/.github/workflows/publish-chart.yaml new file mode 100644 index 0000000..bc5888b --- /dev/null +++ b/.github/workflows/publish-chart.yaml @@ -0,0 +1,35 @@ +name: Release Charts + +on: + push: + branches: + - master + +jobs: + release: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.6.0 + with: + charts_dir: chart + skip_existing: false + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6a52a30 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/norwoodj/helm-docs + rev: v1.12.0 + hooks: + - id: helm-docs-container \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..64ded56 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2024 Brodi Elwood + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f601f69 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# palworld-k8s + +A Helm chart to deploy a Palworld dedicated server. This chart relies on the [`thijsvanloef/palworld-server-docker`](https://github.com/thijsvanloef/palworld-server-docker) image. + +# How to use + +```bash +helm repo add palworld-k8s https://bdelwood.github.io/palworld-k8s/ +helm repo update +helm install palworld-server palworld-k8s/palworld-k8s \ + --set gameServer.serverName=palworld-example \ + --set password=changeme \ +``` + +By default, the server is not configured as a community server, and RCON is enabled. + +The chart will generate default server and admin (RCON) passwords, which can be retrieved with + +```bash +kubectl get secrets -o json | jq '.data | map_values(@base64d)' +``` + +Alternatively, the password can be supplied in the `values` file or by specifying existing secrets. These secrets must have the `admin-password` and `server-password` keys. + +By default, the chart will provision a PVC using the default StorageClass. You can also provide an existing PVC, or change the StorageClass. + +# Configuration + +Full configuration options are detailed in the [Chart readme](/chart/palworld-k8s/README.md). + +# Roadmap + +- [ ] Implement health check/readiness check +- [ ] Support editing server settings diff --git a/chart/palworld-k8s/.helmignore b/chart/palworld-k8s/.helmignore new file mode 100644 index 0000000..691fa13 --- /dev/null +++ b/chart/palworld-k8s/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ \ No newline at end of file diff --git a/chart/palworld-k8s/Chart.yaml b/chart/palworld-k8s/Chart.yaml new file mode 100644 index 0000000..b80aa4a --- /dev/null +++ b/chart/palworld-k8s/Chart.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v2 +description: A basic chart to deploy a Palworld dedicated servers. +name: palworld-k8s +type: application +version: 0.0.0 +appVersion: "v0.17" +kubeVersion: ">=1.26.0-0" +sources: + - https://github.com/bdelwood/palworld-k8s +keywords: + - steam + - palworld + diff --git a/chart/palworld-k8s/README.md b/chart/palworld-k8s/README.md new file mode 100644 index 0000000..44bc6ff --- /dev/null +++ b/chart/palworld-k8s/README.md @@ -0,0 +1,63 @@ +# palworld-k8s + +![Version: 0.0.0](https://img.shields.io/badge/Version-0.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.17](https://img.shields.io/badge/AppVersion-v0.17-informational?style=flat-square) + +A basic chart to deploy a Palworld dedicated servers. + +## Source Code + +* + +## Requirements + +Kubernetes: `>=1.26.0-0` + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | Affinity rules for pod scheduling. | +| fullnameOverride | string | `""` | Override the full name of the chart. Default is a combination of release name and chart name. | +| gameServer.community.enabled | bool | `false` | Enable if you want your server to show up as a community server. | +| gameServer.community.service.nodePort | int | `nil` | Node port a community server (for NodePort service type). | +| gameServer.community.service.port | int | `27015` | Service port for a community server. | +| gameServer.existingSecret | string | `""` | Name of an existing secret for the server password. | +| gameServer.multithreading | bool | `true` | Enable multithreading. | +| gameServer.password | string | `""` | Server password If one is not provided or an existing secret it not provided, one will be generated. | +| gameServer.players | int | `16` | Number of players allowed on the server concurrently. | +| gameServer.serverDescription | string | `""` | Description for the server. | +| gameServer.serverName | string | `""` | Custom server name. | +| gameServer.service.nodePort | int | `nil` | Node port for the game server (for NodePort service type). | +| gameServer.service.port | int | `8211` | Service port for the game server. | +| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| image.registry | string | `"docker.io"` | Container registry for the image. | +| image.repository | string | `"thijsvanloef/palworld-server-docker"` | Image repository | +| image.tag | string | `""` | Overrides the image tag. Default is the chart appVersion. | +| nameOverride | string | `""` | Override the name of the chart. Default is the chart name. | +| nodeSelector | object | `{}` | Node selector for pod scheduling. | +| persistence.accessMode | string | `"ReadWriteOnce"` | Access mode for the persistent volume. | +| persistence.enabled | bool | `true` | Enable or disable persistence. | +| persistence.existingClaim | string | `""` | Name of an existing persistentVolumeClaim. | +| persistence.preventDelete | bool | `true` | Prevent Helm from deleting the PVC. Some storageClasses (such as the local-path-provisioner installed by default by k3s) have reclaimPolicy: Delete. | +| persistence.size | string | `"12Gi"` | Size of the persistent volume. | +| persistence.storageClassName | string | `""` | Storage class name for the PVC. | +| podAnnotations | object | `{}` | Annotations to add to the pod. | +| podSecurityContext | object | `{}` | Security context for the pod. | +| rcon.enabled | bool | `true` | Enable or disable RCON. | +| rcon.existingSecret | string | `""` | Name of an existing secret for the RCON (admin) password. | +| rcon.password | string | `""` | RCON (admin) password. If one is not provided and RCON is enabled, one will be generated. | +| rcon.service.nodePort | int | `nil` | Node port for RCON (for NodePort service type). | +| rcon.service.port | int | `25575` | Service port for RCON. | +| resources.limits | object | `{"cpu":4,"memory":"16Gi"}` | Resource limits (CPU, Memory) for the server. | +| resources.requests | object | `{"cpu":4,"memory":"8Gi"}` | Resource requests (CPU, Memory) for the server. | +| securityContext | object | `{}` | Security context for the pod containers. | +| service.type | string | `"LoadBalancer"` | Service type (e.g., LoadBalancer, ClusterIP, NodePort) | +| serviceAccount.annotations | object | `{}` | Annotations to add to the service account. | +| serviceAccount.create | bool | `false` | Specifies whether a service account should be created. | +| serviceAccount.name | string | `""` | The name of the service account. | +| tolerations | object | `{}` | Tolerations for pod scheduling. | +| tz | string | `"UTC"` | Timezone setting for the server. | +| updateStrategy | object | `{"type":"Recreate"}` | Update strategy for deployments. | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.12.0](https://github.com/norwoodj/helm-docs/releases/v1.12.0) diff --git a/chart/palworld-k8s/templates/_helpers.tpl b/chart/palworld-k8s/templates/_helpers.tpl new file mode 100644 index 0000000..077f48f --- /dev/null +++ b/chart/palworld-k8s/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "palworld-k8s.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "palworld-k8s.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "palworld-k8s.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "palworld-k8s.labels" -}} +helm.sh/chart: {{ include "palworld-k8s.chart" . }} +{{ include "palworld-k8s.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "palworld-k8s.selectorLabels" -}} +app.kubernetes.io/name: {{ include "palworld-k8s.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "palworld-k8s.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "palworld-k8s.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/chart/palworld-k8s/templates/configmap.yaml b/chart/palworld-k8s/templates/configmap.yaml new file mode 100644 index 0000000..c4358c1 --- /dev/null +++ b/chart/palworld-k8s/templates/configmap.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "palworld-k8s.fullname" . }} + labels: + {{- include "palworld-k8s.labels" . | nindent 4 }} +data: + TZ: "{{ .Values.tz }}" + PLAYERS: "{{ .Values.gameServer.players | int }}" + MULTITHREADING: "{{ .Values.gameServer.multithreading }}" + COMMUNITY: "{{ .Values.gameServer.community.enabled }}" + PUBLIC_IP: "" + PUBLIC_PORT: "" + SERVER_NAME: "{{ regexReplaceAll "\\W+" (default (printf "%s_%s" "palworld" (randAlphaNum 6 | nospace)) .Values.gameServer.serverName) "_" }}" + SERVER_DESCRIPTION: "{{ .Values.gameServer.serverDescription }}" + RCON_ENABLED: "{{ .Values.rcon.enabled }}" diff --git a/chart/palworld-k8s/templates/deployment.yaml b/chart/palworld-k8s/templates/deployment.yaml new file mode 100644 index 0000000..d70bd9b --- /dev/null +++ b/chart/palworld-k8s/templates/deployment.yaml @@ -0,0 +1,90 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "palworld-k8s.fullname" . }} + labels: + {{- include "palworld-k8s.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "palworld-k8s.selectorLabels" . | nindent 6 }} + {{- if .Values.updateStrategy }} + strategy: {{- toYaml .Values.updateStrategy | nindent 4 }} + {{- end }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "palworld-k8s.selectorLabels" . | nindent 8 }} + spec: + serviceAccountName: {{ include "palworld-k8s.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + restartPolicy: Always + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + envFrom: + - configMapRef: + name: {{ include "palworld-k8s.fullname" . }} + env: + - name: SERVER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ default (include "palworld-k8s.fullname" .) .Values.gameServer.existingSecret | quote }} + key: server-password + {{- if .Values.rcon.enabled }} + - name: ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ default (include "palworld-k8s.fullname" .) .Values.rcon.existingSecret | quote }} + key: admin-password + {{- end }} + ports: + - name: gameserver + containerPort: 8211 + protocol: UDP + - name: queryport + containerPort: 27015 + protocol: UDP + - name: rcon + containerPort: 25575 + protocol: TCP + volumeMounts: + {{- if .Values.persistence.enabled }} + - mountPath: /palworld + name: datadir + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- if .Values.persistence.enabled }} + volumes: + - name: datadir + persistentVolumeClaim: + {{- if not .Values.persistence.existingClaim }} + claimName: {{ include "palworld-k8s.fullname" . }} + {{ else }} + claimName: "{{ .Values.persistence.existingClaim }}" + {{- end }} + {{- end }} \ No newline at end of file diff --git a/chart/palworld-k8s/templates/pvc.yaml b/chart/palworld-k8s/templates/pvc.yaml new file mode 100644 index 0000000..ca098c8 --- /dev/null +++ b/chart/palworld-k8s/templates/pvc.yaml @@ -0,0 +1,20 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "palworld-k8s.fullname" . }} + labels: + {{- include "palworld-k8s.labels" . | nindent 4 }} + {{- if .Values.persistence.preventDelete }} + helm.sh/resource-policy: keep + {{- end }} +spec: + {{- if .Values.persistence.storageClassName }} + storageClassName: {{ .Values.persistence.storageClassName }} + {{- end }} + accessModes: + - {{ .Values.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.persistence.size }} +{{- end }} diff --git a/chart/palworld-k8s/templates/secrets.yaml b/chart/palworld-k8s/templates/secrets.yaml new file mode 100644 index 0000000..2960b34 --- /dev/null +++ b/chart/palworld-k8s/templates/secrets.yaml @@ -0,0 +1,19 @@ +{{- if or (not .Values.gameServer.existingSecret) (not .Values.rcon.existingSecret) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "palworld-k8s.fullname" . }} + labels: + {{- include "palworld-k8s.labels" . | nindent 4 }} +type: Opaque +data: +{{- if not .Values.gameServer.existingSecret }} + server-password: {{ default (randAlphaNum 12) .Values.gameServer.password | b64enc | quote }} +{{- end}} +{{- if .Values.rcon.enabled }} +{{- if not .Values.rcon.existingSecret }} + admin-password: {{ default (randAlphaNum 12) .Values.rcon.password | b64enc | quote }} +{{- end}} +{{- end }} +{{- end }} diff --git a/chart/palworld-k8s/templates/service.yaml b/chart/palworld-k8s/templates/service.yaml new file mode 100644 index 0000000..829dc53 --- /dev/null +++ b/chart/palworld-k8s/templates/service.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "palworld-k8s.fullname" . }} + labels: + {{- include "palworld-k8s.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - name: gameserver + port: {{ .Values.gameServer.service.port | int }} + protocol: UDP + targetPort: gameserver + {{- if eq .Values.service.type "NodePort" }} + nodePort: {{ default "" .Values.gameServer.service.nodePort | int }} + {{- end }} + {{- if .Values.gameServer.community.enabled }} + - name: queryport + port: {{ .Values.gameServer.community.service.port | int }} + protocol: UDP + targetPort: queryport + {{- if eq .Values.service.type "NodePort" }} + nodePort: {{ default "" .Values.gameServer.community.service.nodePort | int }} + {{- end }} + {{- end }} + {{- if .Values.rcon.enabled }} + - name: rcon + port: {{ .Values.rcon.service.port | int }} + protocol: TCP + targetPort: rcon + {{- if eq .Values.service.type "NodePort" }} + nodePort: {{ default "" .Values.rcon.service.nodePort | int }} + {{- end }} + {{- end }} + selector: + {{- include "palworld-k8s.selectorLabels" . | nindent 4 }} diff --git a/chart/palworld-k8s/templates/serviceaccount.yaml b/chart/palworld-k8s/templates/serviceaccount.yaml new file mode 100644 index 0000000..941636a --- /dev/null +++ b/chart/palworld-k8s/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "palworld-k8s.serviceAccountName" . }} + labels: + {{- include "palworld-k8s.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/chart/palworld-k8s/values.yaml b/chart/palworld-k8s/values.yaml new file mode 100644 index 0000000..ccb2930 --- /dev/null +++ b/chart/palworld-k8s/values.yaml @@ -0,0 +1,122 @@ +# -- Override the name of the chart. +# Default is the chart name. +nameOverride: "" + +# -- Override the full name of the chart. +# Default is a combination of release name and chart name. +fullnameOverride: "" + +# -- Timezone setting for the server. +tz: "UTC" + +image: + # -- Container registry for the image. + registry: docker.io + # -- Image repository + repository: thijsvanloef/palworld-server-docker + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Overrides the image tag. + # Default is the chart appVersion. + tag: "" + +# -- Update strategy for deployments. +updateStrategy: + type: Recreate + +service: + # -- Service type (e.g., LoadBalancer, ClusterIP, NodePort) + type: LoadBalancer + +gameServer: + # -- Number of players allowed on the server concurrently. + players: 16 + # -- Enable multithreading. + multithreading: true + # -- Custom server name. + serverName: "" + # -- Description for the server. + serverDescription: "" + # -- Name of an existing secret for the server password. + existingSecret: "" + # -- Server password + # If one is not provided or an existing secret it not provided, one will be generated. + password: "" + service: + # -- Service port for the game server. + port: 8211 + # -- (int) Node port for the game server (for NodePort service type). + nodePort: + community: + # -- Enable if you want your server to show up as a community server. + enabled: false + service: + # -- Service port for a community server. + port: 27015 + # -- (int) Node port a community server (for NodePort service type). + nodePort: + +rcon: + # -- Enable or disable RCON. + enabled: true + # -- Name of an existing secret for the RCON (admin) password. + existingSecret: "" + # -- RCON (admin) password. + # If one is not provided and RCON is enabled, one will be generated. + password: "" + service: + # -- Service port for RCON. + port: 25575 + # -- (int) Node port for RCON (for NodePort service type). + nodePort: + +persistence: + # -- Enable or disable persistence. + enabled: true + # -- Name of an existing persistentVolumeClaim. + existingClaim: "" + # -- Storage class name for the PVC. + storageClassName: "" + # -- Access mode for the persistent volume. + accessMode: ReadWriteOnce + # -- Prevent Helm from deleting the PVC. + # Some storageClasses (such as the local-path-provisioner installed by default by k3s) have reclaimPolicy: Delete. + preventDelete: true + # -- Size of the persistent volume. + size: 12Gi + +resources: + # -- Resource limits (CPU, Memory) for the server. + limits: + memory: 16Gi + cpu: 4 + # -- Resource requests (CPU, Memory) for the server. + requests: + memory: 8Gi + cpu: 4 + +serviceAccount: + # -- Specifies whether a service account should be created. + create: false + # -- Annotations to add to the service account. + annotations: {} + # -- The name of the service account. + name: "" + +# -- Node selector for pod scheduling. +nodeSelector: {} + +# -- Affinity rules for pod scheduling. +affinity: {} + +# -- Tolerations for pod scheduling. +tolerations: {} + +# -- Annotations to add to the pod. +podAnnotations: {} + +# -- Security context for the pod. +podSecurityContext: {} + +# -- Security context for the pod containers. +securityContext: {}