Skip to content
Closed
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
174 changes: 174 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,180 @@ The `IDENTITY_SECRET` is displayed (masked) in the Admin panel under **Player Id

GameShelf itself listens on plain HTTP. Terminate TLS at your reverse proxy, ingress controller, or load balancer and forward plain HTTP to GameShelf. If you're using the dedicated subdomain option, your proxy (nginx, Caddy, etc.) handles the certificate — Caddy does this automatically.

## Helm Install (Replicated)

Installing directly via Helm from the Replicated registry (not via KOTS/Embedded Cluster).

### Prerequisites

- A Replicated customer license ID (Vendor Portal → Customers → click customer → License ID)
- A Replicated customer email address
- `helm` v3.8+, `kubectl` pointed at your target cluster

### Install

```bash
# 1. Create the namespace
kubectl create namespace gameshelf

# 2. Create the image pull secret (required for proxied images)
kubectl create secret docker-registry enterprise-pull-secret \
--docker-server=proxy.adamanthony.dev \
--docker-username=<customer-email> \
--docker-password=<license-id> \
-n gameshelf

# 3. Log into the Replicated OCI registry
helm registry login registry.replicated.com \
--username <customer-email> \
--password <license-id>

# 4. Install (NodePort for direct access, integration mode for SDK)
helm install gameshelf \
oci://registry.replicated.com/gameshelf/unstable/gameshelf \
--version <chart-version> \
--namespace gameshelf \
--set adminSecret=<your-admin-password> \
--set "gameshelf-sdk.integration.licenseID=<license-id>" \
--set "gameshelf-sdk.integration.enabled=true" \
--set "gameshelf-sdk.image.registry=proxy.adamanthony.dev" \
--set service.type=NodePort \
--set service.nodePort=30080
```

> The chart version for each release is visible in the Vendor Portal under Releases, or in the GitHub Actions run log.
> The image tag for PR-based releases is `pr-<pr-number>` (e.g. `pr-39`). Pass `--set image.tag=pr-<N>` if the chart appVersion doesn't match.

### Upgrade

```bash
helm upgrade gameshelf \
oci://registry.replicated.com/gameshelf/unstable/gameshelf \
--version <new-chart-version> \
--reuse-values
```

> If changing chart version without a new image, `--reuse-values` is sufficient.
> If the new chart has a new image tag (e.g. new PR number), add `--set image.tag=pr-<N>`.

### Access the app

NodePort (if installed with `service.type=NodePort`):
```bash
kubectl get nodes -o wide # get node IP
# open http://<node-ip>:30080
```

Port-forward (if using ClusterIP):
```bash
kubectl port-forward svc/gameshelf 8080:80 -n gameshelf
# open http://localhost:8080
```

Admin panel: `http://<host>/admin?token=<your-admin-password>`

### Common overrides

| Value | Default | Description |
|-------|---------|-------------|
| `adminSecret` | `changeme` | Admin panel password |
| `siteName` | `GameShelf` | Site name shown in the UI |
| `image.tag` | (chart appVersion) | Override image tag — needed when PR image tag differs from appVersion |
| `service.type` | `ClusterIP` | Set to `NodePort` or `LoadBalancer` to expose externally |
| `service.nodePort` | `""` | NodePort port number (e.g. `30080`) |
| `ingress.enabled` | `false` | Enable ingress |
| `ingress.host` | `""` | Hostname for ingress (required when enabled) |
| `postgresql.enabled` | `true` | Use embedded PostgreSQL; set to `false` for external DB |
| `redis.enabled` | `true` | Use embedded Redis; set to `false` for external Redis |
| `gameshelf-sdk.integration.licenseID` | `""` | License ID for SDK integration mode (direct Helm installs) |
| `gameshelf-sdk.integration.enabled` | `false` | Enable SDK integration mode (direct Helm installs) |
| `gameshelf-sdk.image.registry` | `proxy.replicated.com` | Override SDK image registry — set to `proxy.adamanthony.dev` for custom proxy |

### Known gotchas

- **Image tag mismatch**: PR workflow pushes image as `pr-<N>` but sets `appVersion` to `pr-<N>-<run>`. Always pass `--set image.tag=pr-<N>` explicitly.
- **Pull secret**: KOTS creates `enterprise-pull-secret` automatically. Direct Helm installs require creating it manually before install.
- **SDK integration mode**: Direct Helm installs bypass KOTS license injection. Always pass `gameshelf-sdk.integration.licenseID` and `gameshelf-sdk.integration.enabled=true`. The value path is `integration.licenseID`, NOT `integrationLicenseID`.
- **SDK image proxy**: The SDK subchart defaults to `proxy.replicated.com` for its own image. Override with `gameshelf-sdk.image.registry=proxy.adamanthony.dev` to satisfy the custom proxy requirement.
- **Stale pull secret**: If `enterprise-pull-secret` exists from a prior session, delete and recreate it — credentials may be stale.

## Preflight Checks

Run against the live cluster:
```bash
kubectl preflight secret/gameshelf/gameshelf-preflight
```

### Demo failure scenario (helm template — no cluster changes needed)

```bash
helm template gameshelf chart/gameshelf \
--set preflight.requiredEndpoint=https://bad.example.invalid \
--set preflight.minCPU=9999 \
--set preflight.minMemory=9999Gi \
--set preflight.minKubernetesVersion=99.99.0 \
--set postgresql.enabled=false \
--set externalDatabase.host=db.example.invalid \
--set externalDatabase.port=5432 \
-s templates/preflight.yaml | kubectl preflight -
```

### Demo pass scenario

```bash
kubectl preflight secret/gameshelf/gameshelf-preflight
```

## Support Bundle

### Run from laptop (most analyzers; health check will timeout — expected)

```bash
kubectl support-bundle --load-cluster-specs --namespace gameshelf
```

### Run from inside cluster (health check passes; requires RBAC setup below)

```bash
# One-time RBAC setup
kubectl create role support-bundle-role \
--verb=get,list \
--resource=secrets,pods,pods/log,configmaps \
-n gameshelf

kubectl create rolebinding support-bundle-binding \
--role=support-bundle-role \
--serviceaccount=gameshelf:default \
-n gameshelf

# Run bundle
kubectl delete pod support-bundle-runner -n gameshelf 2>/dev/null
kubectl run support-bundle-runner -it --rm \
--image=replicated/troubleshoot:latest \
--restart=Never \
-n gameshelf \
-- support-bundle secret/gameshelf/gameshelf-support-bundle
```

### Demo: induce deploymentStatus failure (3.4)

```bash
kubectl scale deployment gameshelf -n gameshelf --replicas=0
# run bundle — gameshelf Status will be red with actionable message
kubectl scale deployment gameshelf -n gameshelf --replicas=1
```

### Demo: induce DB error textAnalyze (3.5)

```bash
# Point app at non-existent DB — generates connection refused logs
helm upgrade gameshelf ... --set postgresql.enabled=false \
--set externalDatabase.host=db.example.invalid \
--set externalDatabase.port=5432
# run bundle — Database Connection Errors analyzer fires
# restore with postgresql.enabled=true when done
```

## Architecture

```
Expand Down
3 changes: 2 additions & 1 deletion chart/gameshelf/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ appVersion: "0.1.0"

dependencies:
- name: replicated
alias: gameshelf-sdk
version: ~1.19
repository: oci://registry.replicated.com/library
condition: replicated.enabled
condition: gameshelf-sdk.enabled
- name: postgresql
version: ~18.5
repository: https://charts.bitnami.com/bitnami
Expand Down
2 changes: 1 addition & 1 deletion chart/gameshelf/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ spec:
name: {{ include "gameshelf.fullname" . }}
key: admin-secret
- name: SDK_SERVICE_URL
value: "http://replicated:3000"
value: "http://gameshelf-sdk:3000"
livenessProbe:
httpGet:
path: /healthz
Expand Down
2 changes: 1 addition & 1 deletion chart/gameshelf/templates/preflight.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ stringData:
analyzers:
- textAnalyze:
checkName: External Database Connectivity
fileName: db-connection-check/check.log
fileName: db-connection-check.log
regex: "CONNECTION_OK"
exclude: {{ .Values.postgresql.enabled }}
outcomes:
Expand Down
10 changes: 5 additions & 5 deletions chart/gameshelf/templates/support-bundle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ stringData:
maxLines: 5000
maxAge: 72h
- logs:
collectorName: replicated-sdk
collectorName: gameshelf-sdk
selector:
- app=replicated
- app=gameshelf-sdk
namespace: {{ .Release.Namespace }}
limits:
maxLines: 5000
Expand All @@ -68,7 +68,7 @@ stringData:
message: "GameShelf health endpoint is not responding or returning an unhealthy status. Check the gameshelf-app logs for startup errors or crashes."
- textAnalyze:
checkName: Database Connection Errors
fileName: gameshelf-app/*.log
fileName: "*/gameshelf.log"
regex: "connection refused|no such host|dial tcp.*connect: connection refused"
outcomes:
- fail:
Expand All @@ -87,12 +87,12 @@ stringData:
- pass:
message: GameShelf deployment is running with at least one available replica.
- deploymentStatus:
name: replicated
name: gameshelf-sdk
namespace: {{ .Release.Namespace }}
outcomes:
- fail:
when: "< 1"
message: "The Replicated SDK deployment has no available replicas. License validation and entitlement checks may not work. Check the replicated pod logs."
message: "The Replicated SDK deployment has no available replicas. License validation and entitlement checks may not work. Check the gameshelf-sdk pod logs."
- pass:
message: Replicated SDK deployment is running.
- statefulsetStatus:
Expand Down
12 changes: 7 additions & 5 deletions chart/gameshelf/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ global:
imagePullSecrets: []

imageProxy:
host: proxy.replicated.com
host: proxy.adamanthony.dev
appSlug: gameshelf

image:
Expand Down Expand Up @@ -68,10 +68,10 @@ adminSecret: "changeme" # REQUIRED — set a strong secret, e.g. --set adminSec
postgresql:
enabled: true
image:
registry: proxy.replicated.com/proxy/gameshelf/index.docker.io
registry: proxy.adamanthony.dev/proxy/gameshelf/index.docker.io
volumePermissions:
image:
registry: proxy.replicated.com/proxy/gameshelf/index.docker.io
registry: proxy.adamanthony.dev/proxy/gameshelf/index.docker.io
auth:
database: gameshelf
username: gameshelf
Expand Down Expand Up @@ -99,7 +99,7 @@ externalDatabase:
redis:
enabled: true
image:
registry: proxy.replicated.com/proxy/gameshelf/index.docker.io
registry: proxy.adamanthony.dev/proxy/gameshelf/index.docker.io
architecture: standalone
auth:
enabled: false
Expand All @@ -120,8 +120,10 @@ preflight:
minMemory: 4Gi
requiredEndpoint: "https://replicated.app"

replicated:
gameshelf-sdk:
enabled: true
image:
registry: proxy.adamanthony.dev

# --- BYO Redis ---
externalRedis:
Expand Down
6 changes: 4 additions & 2 deletions helmchart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ spec:
name: gameshelf
chartVersion: "0.0.0"
values:
replicated:
integrationLicenseID: repl{{ LicenseFieldValue `licenseID` }}
gameshelf-sdk:
integration:
licenseID: repl{{ LicenseFieldValue `licenseID` }}
enabled: true
adminSecret: repl{{ ConfigOption `admin_secret`}}
siteName: repl{{ ConfigOption `site_name`}}
builder:
Expand Down
33 changes: 33 additions & 0 deletions internal/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ func (s *Server) adminHandler(w http.ResponseWriter, r *http.Request) {
data.AllGames = games
data.DBScores = allScores
data.Token = r.URL.Query().Get("token") // preserve token for form actions
data.SupportBundleSlug = r.URL.Query().Get("bundle")
data.SupportBundleError = r.URL.Query().Get("bundle_error")

// Mask the identity secret for display.
if secret, err := s.getOrCreateIdentitySecret(); err == nil && secret != "" {
Expand Down Expand Up @@ -139,6 +141,37 @@ func (s *Server) logoHandler(w http.ResponseWriter, r *http.Request) {
w.Write(data) //nolint:errcheck
}

// POST /admin/support-bundle — trigger support bundle collection and upload to Vendor Portal
func (s *Server) supportBundleHandler(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
q := url.Values{}
if token != "" {
q.Set("token", token)
}

if !s.sdk.Available() {
q.Set("bundle_error", "SDK unavailable — support bundle upload requires the Replicated SDK sidecar")
http.Redirect(w, r, "/admin?"+q.Encode(), http.StatusSeeOther)
return
}

licenseInfo, _ := s.sdk.GetLicenseInfo(r.Context())
result, err := s.sdk.TriggerSupportBundleUpload(r.Context(), licenseInfo)
if err != nil {
log.Printf("admin: support bundle: %v", err)
q.Set("bundle_error", err.Error())
http.Redirect(w, r, "/admin?"+q.Encode(), http.StatusSeeOther)
return
}

id := result.Slug
if id == "" {
id = result.BundleID
}
q.Set("bundle", id)
http.Redirect(w, r, "/admin?"+q.Encode(), http.StatusSeeOther)
}

// POST /admin/logo — upload a new logo image
func (s *Server) uploadLogoHandler(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 2<<20) // 2MB
Expand Down
3 changes: 3 additions & 0 deletions internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ type PageData struct {
AllGames []db.Game
Site *db.Site
IdentitySecretMasked string // shown (masked) on admin panel
// Support bundle result (populated from query params after POST /admin/support-bundle)
SupportBundleSlug string
SupportBundleError string
}

// pageBase fills the branding fields from the DB and SDK banner state.
Expand Down
1 change: 1 addition & 0 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func (s *Server) Handler() http.Handler {
r.Post("/admin/branding", s.updateBrandingHandler)
r.Post("/admin/logo", s.uploadLogoHandler)
r.Post("/admin/identity/regenerate", s.regenerateIdentitySecretHandler)
r.Post("/admin/support-bundle", s.supportBundleHandler)
})

// Health
Expand Down
Loading
Loading