From ec6c121939e2d92da0b32fedf4205fefd9fc348d Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Sun, 12 Apr 2026 13:34:07 -0400 Subject: [PATCH 1/9] fix: add postgresql and redis statusInformers for Tier 2 instance health reporting KOTS instance reporting was only tracking the gameshelf deployment. Adding PostgreSQL and Redis StatefulSets so all major components appear healthy in Vendor Portal instance details (rubric 2.10). Co-Authored-By: Claude Sonnet 4.6 --- kots-app.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kots-app.yaml b/kots-app.yaml index d6c22e4..b7e694a 100644 --- a/kots-app.yaml +++ b/kots-app.yaml @@ -6,3 +6,5 @@ spec: title: GameShelf statusInformers: - deployment/gameshelf + - statefulset/gameshelf-postgresql + - statefulset/gameshelf-redis-master From 031380cbc50700a4823c44a9c2202bb94d2719d1 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Sun, 12 Apr 2026 13:38:45 -0400 Subject: [PATCH 2/9] feat: restore gameshelf-sdk alias for Tier 2 demo (Helm/KOTS path) Re-adds the alias so the Replicated SDK deployment is named gameshelf-sdk as required by rubric 2.1. Also updates all references (SDK_SERVICE_URL, support-bundle selectors/analyzers, values key, helmchart.yaml key) to match. This intentionally reverts the alias removal from fix/sdk-alias for the Helm install demo path. The EC license injection issue is deferred to a separate track. Co-Authored-By: Claude Sonnet 4.6 --- chart/gameshelf/Chart.yaml | 3 ++- chart/gameshelf/templates/deployment.yaml | 2 +- chart/gameshelf/templates/support-bundle.yaml | 8 ++++---- chart/gameshelf/values.yaml | 2 +- helmchart.yaml | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/chart/gameshelf/Chart.yaml b/chart/gameshelf/Chart.yaml index 81eb42d..ccba752 100644 --- a/chart/gameshelf/Chart.yaml +++ b/chart/gameshelf/Chart.yaml @@ -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 diff --git a/chart/gameshelf/templates/deployment.yaml b/chart/gameshelf/templates/deployment.yaml index d641a35..ccbd2cc 100644 --- a/chart/gameshelf/templates/deployment.yaml +++ b/chart/gameshelf/templates/deployment.yaml @@ -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 diff --git a/chart/gameshelf/templates/support-bundle.yaml b/chart/gameshelf/templates/support-bundle.yaml index 48c27f0..ecaceaa 100644 --- a/chart/gameshelf/templates/support-bundle.yaml +++ b/chart/gameshelf/templates/support-bundle.yaml @@ -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 @@ -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: diff --git a/chart/gameshelf/values.yaml b/chart/gameshelf/values.yaml index 28ba7da..43063af 100644 --- a/chart/gameshelf/values.yaml +++ b/chart/gameshelf/values.yaml @@ -120,7 +120,7 @@ preflight: minMemory: 4Gi requiredEndpoint: "https://replicated.app" -replicated: +gameshelf-sdk: enabled: true # --- BYO Redis --- diff --git a/helmchart.yaml b/helmchart.yaml index 94e52b1..95aa549 100644 --- a/helmchart.yaml +++ b/helmchart.yaml @@ -7,7 +7,7 @@ spec: name: gameshelf chartVersion: "0.0.0" values: - replicated: + gameshelf-sdk: integrationLicenseID: repl{{ LicenseFieldValue `licenseID` }} adminSecret: repl{{ ConfigOption `admin_secret`}} siteName: repl{{ ConfigOption `site_name`}} From ea0480d8a6f502198729e1fb0c37a53795268ea9 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Sun, 12 Apr 2026 13:40:11 -0400 Subject: [PATCH 3/9] fix: use custom proxy domain proxy.adamanthony.dev for all images Replaces proxy.replicated.com with the configured custom domain so all pod images satisfy rubric 2.2 (images must start with your custom proxy domain, not the generic proxy.replicated.com). Co-Authored-By: Claude Sonnet 4.6 --- chart/gameshelf/values.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chart/gameshelf/values.yaml b/chart/gameshelf/values.yaml index 43063af..1a3cfea 100644 --- a/chart/gameshelf/values.yaml +++ b/chart/gameshelf/values.yaml @@ -10,7 +10,7 @@ global: imagePullSecrets: [] imageProxy: - host: proxy.replicated.com + host: proxy.adamanthony.dev appSlug: gameshelf image: @@ -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 @@ -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 From 21abfff2e9773936f87fed1d83efb100181693c3 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Sun, 12 Apr 2026 15:10:44 -0400 Subject: [PATCH 4/9] fix: correct SDK integration mode values and document helm install - helmchart.yaml: use integration.licenseID / integration.enabled (correct SDK subchart value path, not integrationLicenseID) - README: add Helm Install section with full install/upgrade commands, pull secret setup, and common value overrides Co-Authored-By: Claude Sonnet 4.6 --- README.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ helmchart.yaml | 4 ++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 11d4b65..6bb00e3 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,78 @@ 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= \ + --docker-password= \ + -n gameshelf + +# 3. Log into the Replicated OCI registry +helm registry login registry.replicated.com \ + --username \ + --password + +# 4. Install +helm install gameshelf \ + oci://registry.replicated.com/gameshelf/unstable/gameshelf \ + --version \ + --namespace gameshelf \ + --set adminSecret= \ + --set "gameshelf-sdk.integration.licenseID=" \ + --set "gameshelf-sdk.integration.enabled=true" +``` + +> The chart version for each release is visible in the Vendor Portal under Releases, or in the GitHub Actions run log. + +### Upgrade + +```bash +helm upgrade gameshelf \ + oci://registry.replicated.com/gameshelf/unstable/gameshelf \ + --version \ + --reuse-values +``` + +### Access the app + +```bash +kubectl port-forward svc/gameshelf 8080:80 -n gameshelf +``` + +Then open http://localhost:8080. Admin panel: http://localhost:8080/admin?token= + +### Common overrides + +| Value | Default | Description | +|-------|---------|-------------| +| `adminSecret` | `changeme` | Admin panel password | +| `siteName` | `GameShelf` | Site name shown in the UI | +| `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) | + ## Architecture ``` diff --git a/helmchart.yaml b/helmchart.yaml index 95aa549..0ebdada 100644 --- a/helmchart.yaml +++ b/helmchart.yaml @@ -8,7 +8,9 @@ spec: chartVersion: "0.0.0" values: gameshelf-sdk: - integrationLicenseID: repl{{ LicenseFieldValue `licenseID` }} + integration: + licenseID: repl{{ LicenseFieldValue `licenseID` }} + enabled: true adminSecret: repl{{ ConfigOption `admin_secret`}} siteName: repl{{ ConfigOption `site_name`}} builder: From 464d59d26126d24eeb4e41c765bd7f8f55b9f0a0 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Sun, 12 Apr 2026 15:22:46 -0400 Subject: [PATCH 5/9] fix: proxy SDK image through custom domain for rubric 2.2 Set gameshelf-sdk.image.registry to proxy.adamanthony.dev so the SDK pod image also starts with the custom proxy domain. Without this the SDK pulls directly from proxy.replicated.com, failing the 2.2 requirement that every app image uses the custom proxy. Co-Authored-By: Claude Sonnet 4.6 --- chart/gameshelf/values.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chart/gameshelf/values.yaml b/chart/gameshelf/values.yaml index 1a3cfea..c300783 100644 --- a/chart/gameshelf/values.yaml +++ b/chart/gameshelf/values.yaml @@ -122,6 +122,8 @@ preflight: gameshelf-sdk: enabled: true + image: + registry: proxy.adamanthony.dev # --- BYO Redis --- externalRedis: From 5111b217efe4ea7c1d99d9588be469f30b13bc78 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Sun, 12 Apr 2026 16:29:07 -0400 Subject: [PATCH 6/9] fix: correct textAnalyze fileName for app log path The logs collector writes to /.log, not /.log. The gameshelf app pod logs land at e.g. gameshelf-66b5bdfddc-8d52j/gameshelf.log, so the glob gameshelf-app/*.log never matched anything. Use */gameshelf.log to match the container name regardless of pod name hash. Co-Authored-By: Claude Sonnet 4.6 --- chart/gameshelf/templates/support-bundle.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chart/gameshelf/templates/support-bundle.yaml b/chart/gameshelf/templates/support-bundle.yaml index ecaceaa..209fd80 100644 --- a/chart/gameshelf/templates/support-bundle.yaml +++ b/chart/gameshelf/templates/support-bundle.yaml @@ -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: From 0fee277d8463aa0d5c04be951a5d249f597dd11c Mon Sep 17 00:00:00 2001 From: mayor Date: Sun, 12 Apr 2026 17:07:51 -0400 Subject: [PATCH 7/9] feat: add Generate Support Bundle button to admin panel (Tier 3.7) Adds POST /admin/support-bundle route that collects lightweight app diagnostics (app-info.json, license.json), packs them into a tar.gz in memory using stdlib, and uploads to the Vendor Portal via the Replicated SDK sidecar (POST /api/v1/supportbundle). Result (bundle ID or error) is shown inline on the admin panel. Note: uses an in-memory diagnostic bundle rather than kubectl support-bundle to avoid adding the troubleshoot library (~267 deps, 50-100MB binary impact) or requiring the binary in the container image. In production, collect a real bundle and POST the archive to the SDK. Co-Authored-By: Claude Sonnet 4.6 --- internal/api/admin.go | 33 +++++++++ internal/api/handlers.go | 3 + internal/api/server.go | 1 + internal/sdk/supportbundle.go | 124 ++++++++++++++++++++++++++++++++++ templates/admin.html | 26 +++++++ 5 files changed, 187 insertions(+) create mode 100644 internal/sdk/supportbundle.go diff --git a/internal/api/admin.go b/internal/api/admin.go index 4e47c01..b9d89c3 100644 --- a/internal/api/admin.go +++ b/internal/api/admin.go @@ -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 != "" { @@ -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 diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 0a863ac..1e0041c 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -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. diff --git a/internal/api/server.go b/internal/api/server.go index 6b8d4ed..14a0225 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -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 diff --git a/internal/sdk/supportbundle.go b/internal/sdk/supportbundle.go new file mode 100644 index 0000000..635cac2 --- /dev/null +++ b/internal/sdk/supportbundle.go @@ -0,0 +1,124 @@ +package sdk + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" +) + +// SupportBundleResult holds the response from the SDK after a successful upload. +type SupportBundleResult struct { + BundleID string `json:"bundleId"` + Slug string `json:"slug"` +} + +// TriggerSupportBundleUpload collects lightweight app diagnostics, packs them +// into a tar.gz archive, and uploads it to the Vendor Portal via the SDK sidecar. +// Returns a zero SupportBundleResult and nil error when the SDK is unavailable. +// +// NOTE: In production, generate a real bundle with kubectl support-bundle and +// POST the resulting archive to POST /api/v1/supportbundle instead. +func (c *Client) TriggerSupportBundleUpload(ctx context.Context, licenseInfo *LicenseInfo) (SupportBundleResult, error) { + if !c.Available() { + return SupportBundleResult{}, nil + } + + buf, err := buildBundle(licenseInfo) + if err != nil { + return SupportBundleResult{}, fmt.Errorf("sdk: build support bundle: %w", err) + } + + contentLength := int64(buf.Len()) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.base+"/api/v1/supportbundle", buf) + if err != nil { + return SupportBundleResult{}, fmt.Errorf("sdk: build support bundle request: %w", err) + } + req.Header.Set("Content-Type", "application/gzip") + req.ContentLength = contentLength + + // The SDK upload flow makes multiple outbound calls (get presigned URL → S3 PUT → + // mark uploaded), so we use a longer timeout than the default 3s client. + uploadClient := &http.Client{Timeout: 30 * time.Second} + log.Printf("sdk: POST /api/v1/supportbundle (%d bytes)", contentLength) + resp, err := uploadClient.Do(req) + if err != nil { + return SupportBundleResult{}, fmt.Errorf("sdk: support bundle upload: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusCreated { + return SupportBundleResult{}, fmt.Errorf("sdk: support bundle upload returned %d: %s", resp.StatusCode, body) + } + + var result SupportBundleResult + if err := json.Unmarshal(body, &result); err != nil { + return SupportBundleResult{}, fmt.Errorf("sdk: decode support bundle response: %w", err) + } + log.Printf("sdk: support bundle uploaded: id=%s slug=%s", result.BundleID, result.Slug) + return result, nil +} + +// buildBundle creates an in-memory tar.gz with lightweight app diagnostics. +func buildBundle(licenseInfo *LicenseInfo) (*bytes.Buffer, error) { + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + tw := tar.NewWriter(gz) + + appInfo := map[string]interface{}{ + "collectedAt": time.Now().UTC().Format(time.RFC3339), + "source": "gameshelf-app", + "note": "Lightweight diagnostic bundle collected by the GameShelf application. In production, use kubectl support-bundle for a full cluster bundle.", + } + if err := addJSONFile(tw, "bundle/app-info.json", appInfo); err != nil { + return nil, err + } + + if licenseInfo != nil { + licenseData := map[string]interface{}{ + "licenseID": licenseInfo.LicenseID, + "licenseType": licenseInfo.LicenseType, + "customerName": licenseInfo.CustomerName, + "isExpired": licenseInfo.IsExpired, + } + if licenseInfo.ExpirationDate != nil { + licenseData["expirationDate"] = licenseInfo.ExpirationDate.Format(time.RFC3339) + } + if err := addJSONFile(tw, "bundle/license.json", licenseData); err != nil { + return nil, err + } + } + + if err := tw.Close(); err != nil { + return nil, fmt.Errorf("close tar: %w", err) + } + if err := gz.Close(); err != nil { + return nil, fmt.Errorf("close gzip: %w", err) + } + return &buf, nil +} + +// addJSONFile writes v as an indented JSON file into the tar archive. +func addJSONFile(tw *tar.Writer, name string, v interface{}) error { + data, err := json.MarshalIndent(v, "", " ") + if err != nil { + return fmt.Errorf("marshal %s: %w", name, err) + } + hdr := &tar.Header{ + Name: name, + Mode: 0644, + Size: int64(len(data)), + ModTime: time.Now().UTC(), + } + if err := tw.WriteHeader(hdr); err != nil { + return fmt.Errorf("tar header %s: %w", name, err) + } + _, err = tw.Write(data) + return err +} diff --git a/templates/admin.html b/templates/admin.html index 795e1ef..3ec89b5 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -105,6 +105,32 @@

Player Identity

+ +
+

Support

+

+ Generate a diagnostic bundle and upload it automatically to the Vendor Portal. + The bundle will appear on the Instance Details page for this customer. +

+ {{ if .SupportBundleSlug }} +
+ Bundle uploaded successfully. Bundle ID: {{ .SupportBundleSlug }} +
+ {{ end }} + {{ if .SupportBundleError }} +
+ {{ .SupportBundleError }} +
+ {{ end }} +
+ +
+
+

Games

From 8fa3f6cb9475d7d743d9bd784c2de8d81956e5bb Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Sun, 12 Apr 2026 17:14:53 -0400 Subject: [PATCH 8/9] fix: correct runPod textAnalyze fileName for DB connectivity preflight runPod collector with collectorName db-connection-check writes to db-connection-check.log at the root, not db-connection-check/check.log. Co-Authored-By: Claude Sonnet 4.6 --- chart/gameshelf/templates/preflight.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chart/gameshelf/templates/preflight.yaml b/chart/gameshelf/templates/preflight.yaml index f6a5d1d..9c765e0 100644 --- a/chart/gameshelf/templates/preflight.yaml +++ b/chart/gameshelf/templates/preflight.yaml @@ -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: From 5fcbbbfe521e5513dddd2f7d5e7cd83bd07d97aa Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Sun, 12 Apr 2026 17:47:53 -0400 Subject: [PATCH 9/9] docs: add preflight, support bundle demo commands and known gotchas Expands the Helm Install section with: - Full install command including SDK proxy, NodePort, integration mode - Known gotchas: image tag mismatch, pull secret, SDK integration mode value path, SDK image proxy override - Preflight demo commands: fail scenario via helm template, pass scenario - Support bundle: laptop vs in-cluster approaches, RBAC setup, deploymentStatus failure induction (3.4), DB error induction (3.5) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6bb00e3..01a5a7f 100644 --- a/README.md +++ b/README.md @@ -247,17 +247,21 @@ helm registry login registry.replicated.com \ --username \ --password -# 4. Install +# 4. Install (NodePort for direct access, integration mode for SDK) helm install gameshelf \ oci://registry.replicated.com/gameshelf/unstable/gameshelf \ --version \ --namespace gameshelf \ --set adminSecret= \ --set "gameshelf-sdk.integration.licenseID=" \ - --set "gameshelf-sdk.integration.enabled=true" + --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-` (e.g. `pr-39`). Pass `--set image.tag=pr-` if the chart appVersion doesn't match. ### Upgrade @@ -268,13 +272,24 @@ helm upgrade gameshelf \ --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-`. + ### Access the app +NodePort (if installed with `service.type=NodePort`): +```bash +kubectl get nodes -o wide # get node IP +# open http://:30080 +``` + +Port-forward (if using ClusterIP): ```bash kubectl port-forward svc/gameshelf 8080:80 -n gameshelf +# open http://localhost:8080 ``` -Then open http://localhost:8080. Admin panel: http://localhost:8080/admin?token= +Admin panel: `http:///admin?token=` ### Common overrides @@ -282,6 +297,7 @@ Then open http://localhost:8080. Admin panel: http://localhost:8080/admin?token= |-------|---------|-------------| | `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 | @@ -290,6 +306,92 @@ Then open http://localhost:8080. Admin panel: http://localhost:8080/admin?token= | `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-` but sets `appVersion` to `pr--`. Always pass `--set image.tag=pr-` 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