Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/helm-lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Helm Lint

on:
push:
branches: [main]
paths:
- "chart/**"
- ".github/workflows/helm-lint.yaml"
pull_request:
paths:
- "chart/**"
- ".github/workflows/helm-lint.yaml"

permissions:
contents: read

jobs:
lint-helm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- uses: azure/setup-helm@f0accbfd55e3332a28f721b8202b1016cecf90d5 # v5

- name: Lint Helm chart
run: helm lint chart/pelican-wings
16 changes: 16 additions & 0 deletions chart/pelican-wings/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v2
name: pelican-wings
description: Helm chart for Pelican Wings — the game server management daemon with Kubernetes support
type: application
version: 0.1.0
appVersion: "1.0.0"
keywords:
- pelican
- wings
- game-server
- kubernetes
maintainers:
- name: Exonical
url: https://github.com/Exonical
sources:
- https://github.com/pelican-dev/wings
146 changes: 146 additions & 0 deletions chart/pelican-wings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Pelican Wings Helm Chart

Deploys [Pelican Wings](https://github.com/pelican-dev/wings) — the game server
management daemon — into a Kubernetes cluster with full RBAC, storage, and
networking support.

## Prerequisites

- Kubernetes 1.34+
- Helm 3.x
- A running [Pelican Panel](https://github.com/pelican-dev/panel) instance

## Quick Start

Put your node credentials from the Panel in a local values file (kept out of
version control) rather than on the command line, where `--set` would leak them
into shell history and process listings:

```yaml
# values.local.yaml (do not commit)
wings:
panelUrl: https://panel.example.com
token: YOUR_TOKEN
tokenId: YOUR_TOKEN_ID
uuid: YOUR_NODE_UUID
```

```bash
helm install wings ./chart/pelican-wings -f values.local.yaml
```

## Configuration

See [values.yaml](values.yaml) for the full list of configurable values.

### Key Values

| Parameter | Description | Default |
|-----------|-------------|---------|
| `wings.panelUrl` | Panel URL | `https://panel.example.com` |
| `wings.token` | Panel authentication token | `""` |
| `wings.tokenId` | Panel token ID | `""` |
| `wings.uuid` | Node UUID from Panel | `""` |
| `wings.kubernetes.networkMode` | Port exposure: `hostport`, `nodeport`, or `loadbalancer` | `nodeport` |
| `wings.kubernetes.storageMode` | Storage: `hostpath` or `pvc` | `pvc` |
| `wings.kubernetes.storageClass` | StorageClass for PVCs | `""` (cluster default) |
| `wings.kubernetes.storageSize` | Default PVC size | `10Gi` |
| `wings.kubernetes.imagePullPolicy` | Pull policy for game server Pods/install Jobs: `Always`, `IfNotPresent`, `Never` | `""` (smart default) |
| `gameNamespace` | Namespace for game server resources | `pelican` |
| `rbac.create` | Create RBAC resources | `true` |
| `rbac.kubeletMetricsFallback` | Grant cluster-wide `nodes/proxy` for the kubelet stats fallback (broad permission; prefer metrics-server) | `false` |
| `serviceAccount.create` | Create ServiceAccount | `true` |
| `serviceAccount.name` | ServiceAccount name (**required** when `serviceAccount.create=false`) | `""` |

> **Namespace:** Wings schedules game-server workloads into `gameNamespace`, and
> the chart creates the namespaced RBAC there. `wings.kubernetes.namespace` is
> therefore derived from `gameNamespace`; if you set it explicitly it must match
> `gameNamespace` or the chart will fail to render.

> **Credentials:** `wings.token`, `wings.tokenId`, and `wings.uuid` are rendered
> into a Kubernetes **Secret** (not a ConfigMap). Supply them via a private
> values file or `--set`, e.g. `helm install ... -f my-creds.yaml`, and keep that
> file out of version control.

### Storage

By default, the chart uses PVC-based storage (`storageMode: pvc`). This creates
a PersistentVolumeClaim per game server, enabling proper data lifecycle
management.

For single-node setups or testing, you can use HostPath:

```yaml
wings:
kubernetes:
storageMode: hostpath
```

### Networking

NodePort mode (default) creates a Kubernetes Service per game server, exposing
ports via cluster-assigned NodePorts:

```yaml
wings:
kubernetes:
networkMode: nodeport
nodeportPreserve: true # Try to use game port as NodePort
```

HostPort mode binds game server ports directly to the node:

```yaml
wings:
kubernetes:
networkMode: hostport
```

LoadBalancer mode provisions a `Service` of type `LoadBalancer` per game server
(for use with MetalLB, Cilium LB-IPAM, or a cloud LB). LB IP/sharing-key
annotations can be auto-populated from the allocation IP:

```yaml
wings:
kubernetes:
networkMode: loadbalancer
```

### Image pulling

Game server Pods and installation Jobs default to `imagePullPolicy: Always`
for remote images, so updated tags are re-pulled rather than reusing a stale
copy cached on the node (matching the Docker backend). `~`-prefixed local
images are never pulled. Override this for air-gapped clusters:

```yaml
wings:
kubernetes:
imagePullPolicy: IfNotPresent # or "Never"
```

This is independent of `image.pullPolicy`, which applies to the Wings daemon
image itself.

## What Gets Created

- **Namespace** — `pelican` (configurable)
- **ServiceAccount** — For Wings and game server Pods
- **Role + RoleBinding** — Namespace-scoped permissions (Pods, Services, Jobs, PVCs)
- **ClusterRole + ClusterRoleBinding** — Metrics API access (`nodes/proxy` only when `rbac.kubeletMetricsFallback=true`)
- **Secret** — Wings configuration file (contains Panel token)
- **Deployment** — Wings daemon with health probes
- **Service** — Exposes Wings API within the cluster

## Uninstalling

```bash
helm uninstall wings
```

Note: PVCs created for game servers are NOT automatically deleted when
uninstalling the chart. Delete them manually if you want to remove all data:

```bash
kubectl delete pvc -n pelican -l app.kubernetes.io/managed-by=pelican-wings
```
22 changes: 22 additions & 0 deletions chart/pelican-wings/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Pelican Wings has been deployed!

1. Get the Wings API URL:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Values.gameNamespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "pelican-wings.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Values.gameNamespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo "Wings API: http://$NODE_IP:$NODE_PORT"
{{- else if contains "ClusterIP" .Values.service.type }}
kubectl port-forward --namespace {{ .Values.gameNamespace }} svc/{{ include "pelican-wings.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
echo "Wings API: http://127.0.0.1:{{ .Values.service.port }}"
{{- end }}

2. Configure your Panel to connect to this Wings node using the URL above.

3. Verify the deployment:
kubectl get pods --namespace {{ .Values.gameNamespace }} -l "{{ include "pelican-wings.selectorLabels" . | replace "\n" "," }}"

Configuration:
- Game namespace: {{ .Values.gameNamespace }}
- Network mode: {{ .Values.wings.kubernetes.networkMode }}
- Storage mode: {{ .Values.wings.kubernetes.storageMode }}
- Panel URL: {{ .Values.wings.panelUrl }}
62 changes: 62 additions & 0 deletions chart/pelican-wings/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "pelican-wings.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- define "pelican-wings.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 "pelican-wings.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels.
*/}}
{{- define "pelican-wings.labels" -}}
helm.sh/chart: {{ include "pelican-wings.chart" . }}
{{ include "pelican-wings.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels.
*/}}
{{- define "pelican-wings.selectorLabels" -}}
app.kubernetes.io/name: {{ include "pelican-wings.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Create the name of the service account to use.
*/}}
{{- define "pelican-wings.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "pelican-wings.fullname" .) .Values.serviceAccount.name }}
{{- else if .Values.serviceAccount.name }}
{{- .Values.serviceAccount.name }}
{{- else }}
{{- fail "serviceAccount.name must be set when serviceAccount.create is false: binding RBAC to the namespace 'default' ServiceAccount would grant Wings' permissions to every workload using it." }}
{{- end }}
{{- end }}
23 changes: 23 additions & 0 deletions chart/pelican-wings/templates/clusterrole-metrics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{{- if .Values.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "pelican-wings.fullname" . }}-metrics
labels:
{{- include "pelican-wings.labels" . | nindent 4 }}
rules:
- apiGroups: ["metrics.k8s.io"]
resources: ["pods"]
verbs: ["get"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get"]
{{- if .Values.rbac.kubeletMetricsFallback }}
# Proxy to kubelet stats/summary API for resource metrics when
# metrics-server is not installed. Opt-in: this is a broad cluster-scoped
# permission (rbac.kubeletMetricsFallback).
- apiGroups: [""]
resources: ["nodes/proxy"]
verbs: ["get"]
{{- end }}
{{- end }}
16 changes: 16 additions & 0 deletions chart/pelican-wings/templates/clusterrolebinding-metrics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{{- if .Values.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "pelican-wings.fullname" . }}-metrics
labels:
{{- include "pelican-wings.labels" . | nindent 4 }}
subjects:
- kind: ServiceAccount
name: {{ include "pelican-wings.serviceAccountName" . }}
namespace: {{ .Values.gameNamespace }}
roleRef:
kind: ClusterRole
name: {{ include "pelican-wings.fullname" . }}-metrics
apiGroup: rbac.authorization.k8s.io
{{- end }}
Loading