From 6f8b48a73c3bb8a2343a76a18884b87d32b3654f Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 7 Mar 2026 15:06:26 +0000 Subject: [PATCH] [ci skip] k8s portal: fix setup script + add onboarding hub (5 new pages) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug fixes: - CA cert now populated in ConfigMap (was empty → TLS failures) - Remove useless heredoc quote escaping in setup script - Fix homepage: VPN callout, correct verification command (get namespaces) - Fix false-positive sensitive=true on ingress_path, tls_secret_name, truenas_host, ollama_host, client_certificate_secret_name New pages (direct Svelte, no mdsvex dependency): - /onboarding: step-by-step guide (VPN, kubectl, git, first PR) - /architecture: cluster topology, storage, networking, tiers - /services: catalog of 70+ services with URLs - /contributing: PR workflow, what you can/can't change, NEVER list - /troubleshooting: common issues and fixes Navigation bar added to layout. All pages use consistent docs styling. Requires Docker image rebuild: cd stacks/platform/modules/k8s-portal/files && docker build -t viktorbarzin/k8s-portal:latest . && docker push --- config.tfvars | Bin 8734 -> 9933 bytes .../2026-03-07-k8s-portal-onboarding-plan.md | 210 ++++++++++++++++++ stacks/hackmd/main.tf | 1 - stacks/k8s-dashboard/main.tf | 1 - stacks/platform/main.tf | 6 +- .../files/src/routes/+layout.svelte | 53 +++++ .../k8s-portal/files/src/routes/+page.svelte | 37 ++- .../src/routes/architecture/+page.svelte | 73 ++++++ .../src/routes/contributing/+page.svelte | 62 ++++++ .../files/src/routes/onboarding/+page.svelte | 89 ++++++++ .../files/src/routes/services/+page.svelte | 52 +++++ .../files/src/routes/setup/script/+server.ts | 7 +- .../src/routes/troubleshooting/+page.svelte | 63 ++++++ stacks/platform/modules/k8s-portal/main.tf | 7 +- .../modules/reverse_proxy/factory/main.tf | 1 - 15 files changed, 648 insertions(+), 14 deletions(-) create mode 100644 docs/plans/2026-03-07-k8s-portal-onboarding-plan.md create mode 100644 stacks/platform/modules/k8s-portal/files/src/routes/architecture/+page.svelte create mode 100644 stacks/platform/modules/k8s-portal/files/src/routes/contributing/+page.svelte create mode 100644 stacks/platform/modules/k8s-portal/files/src/routes/onboarding/+page.svelte create mode 100644 stacks/platform/modules/k8s-portal/files/src/routes/services/+page.svelte create mode 100644 stacks/platform/modules/k8s-portal/files/src/routes/troubleshooting/+page.svelte diff --git a/config.tfvars b/config.tfvars index 651fed74e42c6655934d9c88adb63c5a1ef32f51..ca9a8141a687fa42dbb71d61baa7bfc247e0311e 100644 GIT binary patch literal 9933 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 LgqXg3tE_tZD;8uA literal 8734 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/dev/null | openssl x509 -issuer -noout` — compare issuer with CA cert subject +3. Add `variable "k8s_ca_cert" { type = string }` to `main.tf` +4. Add the cert value to `config.tfvars` (it's public, not a secret) +5. Use in ConfigMap: `"ca.crt" = var.k8s_ca_cert` +6. Pass through `stacks/platform/main.tf` module call + +**Double-base64 risk**: The Node.js code does `Buffer.from(caCert).toString('base64')` on the PEM text. This creates base64-of-PEM, which kubectl accepts (kubectl handles both base64(PEM) and base64(DER)). Verified: this is the standard kubeconfig format used by `kubectl config set-cluster --certificate-authority`. + +### Bug 2 — Missing VPN prerequisite +**Root cause**: Kubeconfig points to `https://10.0.20.100:6443` (internal IP). No VPN = no connection. + +**Fix**: Add VPN setup as step 0 in both: +- The existing homepage (`+page.svelte`) — prominent callout box +- The new onboarding page — full enrollment instructions + +### Bug 3 — Headscale enrollment is admin-gated +**Fix**: Document the complete flow: +1. User installs Tailscale app +2. User runs `tailscale login --login-server https://headscale.viktorbarzin.me` +3. User sends the registration URL to Viktor (via Slack/email — provide contact) +4. Viktor approves on Headscale +5. User is now on the VPN + +### Bug 4 — `kubectl get pods` vs `kubectl get namespaces` +**Fix**: Change homepage `+page.svelte` to say `kubectl get namespaces` (consistent with setup script). + +### Bug 5 — Unused `openid` scope fix +**NOT a bug**: kubelogin always adds `openid` automatically. Remove from the plan. The real investigation is: verify Authentik's `kubernetes` OIDC provider returns `groups` claim in the ID token. + +### Bug 6 — Heredoc quoting no-op +**Fix**: Remove the useless `escapedKubeconfig` replace on line 49 of `script/+server.ts` — the quoted heredoc delimiter makes it irrelevant. + +### Files to Modify +- `stacks/platform/modules/k8s-portal/main.tf` — add `k8s_ca_cert` variable, update ConfigMap +- `stacks/platform/main.tf` — pass `k8s_ca_cert` to module +- `config.tfvars` — add the CA cert value +- `files/src/routes/setup/script/+server.ts` — remove useless quote escaping +- `files/src/routes/download/+server.ts` — same CA cert fix applies here (identical code) +- `files/src/routes/+page.svelte` — add VPN callout, fix verification command + +--- + +## Part 2: Content System — Skip mdsvex, Use Direct Svelte + +### Why NOT mdsvex +- Svelte 5.53.0 broke mdsvex (unresolved as of today) +- Requires pinning Svelte to <5.53, which conflicts with security updates +- Runes mode in layouts is broken in mdsvex +- The content is 5 small pages authored by one person — mdsvex is overkill +- Build complexity and image size increase for minimal benefit + +### Alternative: Write content directly in Svelte components +Each content page is a Svelte component with inline HTML/text: +```svelte + +
+

Getting Started

+

Welcome! Follow these steps...

+ ... +
+``` + +**Advantages**: +- Zero new dependencies +- Works with any Svelte 5 version +- Content is still just HTML/text in clearly named files +- Can add Svelte interactivity later (copy buttons, progress tracking) + +**Trade-off**: Content edits require touching `.svelte` files instead of `.md`. For 5 pages maintained by one person (or an AI), this is fine. If content grows significantly, revisit mdsvex later when Svelte 5 compatibility is stable. + +### Shared Content Styling +Create `src/lib/content.css` with the docs-style layout: +```css +.content { max-width: 768px; margin: 2rem auto; font-family: system-ui; line-height: 1.6; } +.content h1 { border-bottom: 1px solid #e0e0e0; padding-bottom: 0.5rem; } +.content pre { background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 6px; } +.content code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; } +.content .callout { background: #fff3cd; border-left: 4px solid #ffc107; padding: 1rem; margin: 1rem 0; } +.content .danger { background: #f8d7da; border-left: 4px solid #dc3545; } +``` + +--- + +## Part 3: Route Structure + +``` +src/routes/ +├── +layout.svelte ← Nav bar (Home, Onboarding, Architecture, Services, Contributing, Troubleshooting) +├── +page.svelte ← Identity + VPN callout + Get Started (UPDATED) +├── onboarding/+page.svelte ← Step-by-step guide +├── architecture/+page.svelte ← How the cluster works +├── services/+page.svelte ← Service catalog +├── contributing/+page.svelte ← PR workflow +├── troubleshooting/+page.svelte ← Common issues +├── setup/+page.svelte ← Existing kubectl install +├── setup/script/+server.ts ← Existing auto-setup (FIXED) +└── download/+server.ts ← Existing kubeconfig download (FIXED) +``` + +### Navigation Layout (`+layout.svelte`) +Simple horizontal nav, active page highlighted: +```svelte + + +``` + +--- + +## Part 4: Page Content + +### `/onboarding` — Getting Started (non-technical, step-by-step) + +**Step 0 — Join the VPN** +- "The cluster is on a private network. You need VPN access first." +- Install Tailscale: link to tailscale.com/download +- Run: `tailscale login --login-server https://headscale.viktorbarzin.me` +- "This will open a browser with a registration URL. Send that URL to Viktor via [Slack/email]. He'll approve your device within a few hours." +- "Once approved, you're connected! Test: `ping 10.0.20.100`" + +**Step 1 — Log in to the portal** +- "Visit https://k8s-portal.viktorbarzin.me and sign in with your Authentik account" +- "If you don't have an account, ask Viktor to create one" + +**Step 2 — Set up kubectl** +- macOS: `bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=mac)` +- Linux: `bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=linux)` +- Windows: "Use WSL2 and follow the Linux instructions" +- macOS prerequisite: "Requires Homebrew. Install it first if you don't have it: [link]" + +**Step 3 — Verify access** +- Run: `kubectl get namespaces` +- "This will open a browser for you to log in. After login, you should see a list of namespaces." +- Show expected output example + +**Step 4 — Clone the repo** +- `git clone https://github.com/ViktorBarzin/infra.git` + +**Step 5 — Install your AI assistant (optional)** +- Install Codex: `npm install -g @openai/codex` +- "Codex reads AGENTS.md from the repo and knows how to work with the cluster" + +**Step 6 — Your first change** +- Walk-through: create branch, edit a file, push, open PR, watch CI + +### `/architecture` — How It Works +- Simplified: "Proxmox runs VMs → VMs form a K8s cluster → services run as pods" +- Storage, networking, DNS in plain English +- Tier system: "critical services restart first, optional services restart last" + +### `/services` — What's Running +- Table: service name, URL, what it does +- Top services highlighted (Nextcloud, Grafana, Uptime Kuma, etc.) + +### `/contributing` — How to Contribute +- Branch → edit → PR → review → CI applies +- "What you CAN change" vs "what needs Viktor's review" +- The NEVER list (kubectl apply, secrets in plaintext, NFS restart) + +### `/troubleshooting` — Common Issues +- "Can't connect to the cluster" → VPN + KUBECONFIG +- "Permission denied on kubectl" → namespace access +- "Pod is crashing" → check logs +- "PR CI failed" → read Woodpecker logs +- "Need a new secret" → ask Viktor + +--- + +## Part 5: Build & Deploy + +1. Make code changes (bug fixes + new pages) +2. Build locally: `cd files && npm install && npm run dev` — verify all pages +3. Test kubeconfig: verify CA cert is present and valid +4. Build Docker image: `docker build -t viktorbarzin/k8s-portal:latest .` +5. Push to registry +6. `terragrunt apply` to deploy +7. End-to-end test on a fresh machine + +--- + +## Implementation Order +1. Fix CA cert (immediate — unblocks setup script) +2. Fix homepage (VPN callout, correct verification command) +3. Remove useless heredoc escaping +4. Add nav layout +5. Create 5 content pages (onboarding, architecture, services, contributing, troubleshooting) +6. Build, push, deploy +7. End-to-end test diff --git a/stacks/hackmd/main.tf b/stacks/hackmd/main.tf index debe9a5a..d6eef4ac 100644 --- a/stacks/hackmd/main.tf +++ b/stacks/hackmd/main.tf @@ -4,7 +4,6 @@ variable "hackmd_db_password" { } variable "tls_secret_name" { type = string - sensitive = true } variable "nfs_server" { type = string } variable "mysql_host" { type = string } diff --git a/stacks/k8s-dashboard/main.tf b/stacks/k8s-dashboard/main.tf index 27f90858..09f29c4a 100644 --- a/stacks/k8s-dashboard/main.tf +++ b/stacks/k8s-dashboard/main.tf @@ -4,7 +4,6 @@ variable "tls_secret_name" { } variable "client_certificate_secret_name" { type = string - sensitive = true } diff --git a/stacks/platform/main.tf b/stacks/platform/main.tf index 847ec9bf..f3f3aea8 100644 --- a/stacks/platform/main.tf +++ b/stacks/platform/main.tf @@ -24,7 +24,6 @@ # --- Core --- variable "tls_secret_name" { type = string - sensitive = true } variable "nfs_server" { type = string } variable "redis_host" { type = string } @@ -75,6 +74,10 @@ variable "homepage_credentials" { # --- headscale --- variable "headscale_config" { type = string } variable "headscale_acl" { type = string } +variable "k8s_ca_cert" { + type = string + default = "" +} # --- authentik / rbac / k8s-portal --- variable "authentik_secret_key" { @@ -317,6 +320,7 @@ module "k8s-portal" { source = "./modules/k8s-portal" tier = local.tiers.edge tls_secret_name = var.tls_secret_name + k8s_ca_cert = var.k8s_ca_cert } # ----------------------------------------------------------------------------- diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/+layout.svelte b/stacks/platform/modules/k8s-portal/files/src/routes/+layout.svelte index 9cebde54..d412c4d6 100644 --- a/stacks/platform/modules/k8s-portal/files/src/routes/+layout.svelte +++ b/stacks/platform/modules/k8s-portal/files/src/routes/+layout.svelte @@ -1,5 +1,6 @@ @@ -8,4 +9,56 @@ + + {@render children()} + + diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/+page.svelte b/stacks/platform/modules/k8s-portal/files/src/routes/+page.svelte index b4783b6f..961011cd 100644 --- a/stacks/platform/modules/k8s-portal/files/src/routes/+page.svelte +++ b/stacks/platform/modules/k8s-portal/files/src/routes/+page.svelte @@ -5,6 +5,11 @@

Kubernetes Access Portal

+
+ VPN Required — The cluster is on a private network. You need Headscale VPN access before kubectl will work. + See the Getting Started guide for VPN setup instructions. +
+

Your Identity

Username: {data.username}

@@ -18,18 +23,31 @@

Get Started

    +
  1. Complete the onboarding guide (VPN, kubectl, git)
  2. Install kubectl and kubelogin
  3. Download your kubeconfig
  4. -
  5. Run kubectl get pods to verify access
  6. +
  7. Run kubectl get namespaces to verify access
+ +
+

Resources

+ +
diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/architecture/+page.svelte b/stacks/platform/modules/k8s-portal/files/src/routes/architecture/+page.svelte new file mode 100644 index 00000000..0f6e0734 --- /dev/null +++ b/stacks/platform/modules/k8s-portal/files/src/routes/architecture/+page.svelte @@ -0,0 +1,73 @@ +
+

Architecture

+ +
+

Overview

+

The infrastructure runs on a single Dell R730 server (22 CPU cores, 142GB RAM) using Proxmox to manage virtual machines. Five of those VMs form a Kubernetes cluster that runs 70+ services.

+
+Proxmox (Dell R730)
+ ├── k8s-master  (10.0.20.100) — control plane
+ ├── k8s-node1   (10.0.20.101) — GPU node (Tesla T4)
+ ├── k8s-node2   (10.0.20.102) — worker
+ ├── k8s-node3   (10.0.20.103) — worker
+ ├── k8s-node4   (10.0.20.104) — worker
+ ├── TrueNAS     (10.0.10.15)  — storage (NFS + iSCSI)
+ └── pfSense     (10.0.20.1)   — firewall + gateway
+
+ +
+

Networking

+
    +
  • Public domain: viktorbarzin.me — managed by Cloudflare
  • +
  • Internal domain: viktorbarzin.lan — managed by Technitium DNS
  • +
  • Ingress: Cloudflare → Traefik → services
  • +
  • VPN: Headscale (self-hosted Tailscale)
  • +
+
+ +
+

Storage

+
    +
  • NFS (nfs-truenas) — for app data (files, configs, media). Stored on TrueNAS.
  • +
  • iSCSI (iscsi-truenas) — for databases (PostgreSQL, MySQL). Block storage.
  • +
+
+ +
+

Service Tiers

+

Services are organized into tiers that control resource limits and restart priority:

+ + + + + + + +
TierExamplesPriority
0-coreTraefik, DNS, VPN, AuthHighest — never evicted
1-clusterRedis, Prometheus, CrowdSecHigh
2-gpuOllama, Immich ML, WhisperMedium
3-edgeNextcloud, Paperless, GrafanaNormal
4-auxDashy, PrivateBin, CyberChefLow — evicted first under pressure
+
+ +
+

Infrastructure as Code

+

Everything is managed with Terraform (via Terragrunt). Each service has its own stack:

+
stacks/
+ ├── platform/       ← core infra (22 modules)
+ ├── url/            ← URL shortener (Shlink)
+ ├── immich/         ← photo library
+ ├── nextcloud/      ← file storage
+ └── ... (70+ more)
+

Changes go through git: branch → PR → review → merge → CI applies automatically.

+
+
+ + diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/contributing/+page.svelte b/stacks/platform/modules/k8s-portal/files/src/routes/contributing/+page.svelte new file mode 100644 index 00000000..6f0d1903 --- /dev/null +++ b/stacks/platform/modules/k8s-portal/files/src/routes/contributing/+page.svelte @@ -0,0 +1,62 @@ +
+

How to Contribute

+ +
+

Workflow

+
    +
  1. Create a branch: git checkout -b fix/my-change
  2. +
  3. Make your changes in stacks/<service>/main.tf
  4. +
  5. Push and open a PR: git push -u origin fix/my-change
  6. +
  7. Viktor reviews and merges
  8. +
  9. CI applies automatically — Slack notification when done
  10. +
+
+ +
+

What you CAN change

+
    +
  • Service configurations (image tags, environment variables, resource limits)
  • +
  • New services (add a new stack under stacks/)
  • +
  • Ingress routes, health probes, replica counts
  • +
+
+ +
+

What needs Viktor's review

+
    +
  • CI pipeline changes (.woodpecker/)
  • +
  • Terragrunt configuration (terragrunt.hcl)
  • +
  • Secrets configuration (.sops.yaml)
  • +
  • Core platform modules (stacks/platform/)
  • +
+
+ +
+

NEVER do these

+
+
    +
  • Never kubectl apply/edit/patch — all changes go through Terraform
  • +
  • Never put secrets in code — ask Viktor to add them to the encrypted secrets file
  • +
  • Never restart NFS on TrueNAS — causes cluster-wide mount failures
  • +
  • Never push directly to master — always use a PR
  • +
+
+
+ +
+

Need a new secret?

+

Comment on your PR: "I need a database password for my-service." Viktor will add it to the encrypted secrets file and push to your branch.

+

Then reference it in your Terraform: var.my_service_db_password

+
+
+ + diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/onboarding/+page.svelte b/stacks/platform/modules/k8s-portal/files/src/routes/onboarding/+page.svelte new file mode 100644 index 00000000..8d8fc3d2 --- /dev/null +++ b/stacks/platform/modules/k8s-portal/files/src/routes/onboarding/+page.svelte @@ -0,0 +1,89 @@ +
+

Getting Started

+

Welcome! Follow these steps to get access to the home Kubernetes cluster.

+ +
+

Step 0 — Join the VPN

+

The cluster is on a private network (10.0.20.0/24). You need VPN access first.

+
    +
  1. Install Tailscale for your OS
  2. +
  3. Run this in your terminal: +
    tailscale login --login-server https://headscale.viktorbarzin.me
    +
  4. +
  5. A browser window will open with a registration URL
  6. +
  7. Send that URL to Viktor via email (vbarzin@gmail.com) or Slack
  8. +
  9. Wait for approval (usually within a few hours)
  10. +
  11. Once approved, test:
    ping 10.0.20.100
  12. +
+
+ +
+

Step 1 — Log in to the portal

+

Visit k8s-portal.viktorbarzin.me and sign in with your Authentik account.

+

If you don't have an account yet, ask Viktor to create one.

+
+ +
+

Step 2 — Set up kubectl

+

Run one of these commands in your terminal to install everything automatically:

+

macOS

+

Requires Homebrew. Install it first if you don't have it.

+
bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=mac)
+

Linux

+
bash <(curl -fsSL https://k8s-portal.viktorbarzin.me/setup/script?os=linux)
+

Windows

+

Use WSL2 and follow the Linux instructions.

+
+ +
+

Step 3 — Verify access

+

Run this command. It will open your browser for login the first time:

+
kubectl get namespaces
+

You should see output like:

+
NAME              STATUS   AGE
+default           Active   200d
+kube-system       Active   200d
+monitoring        Active   200d
+...
+

If you get a connection error, make sure your VPN is connected (tailscale status).

+
+ +
+

Step 4 — Clone the repo

+
git clone https://github.com/ViktorBarzin/infra.git
+cd infra
+

This is where all the infrastructure configuration lives.

+
+ +
+

Step 5 — Install your AI assistant (optional)

+

Install Codex CLI for AI-assisted cluster management:

+
npm install -g @openai/codex
+

Codex reads the AGENTS.md file in the repo and knows how to work with the cluster.

+
+ +
+

Step 6 — Your first change

+
    +
  1. Create a branch:
    git checkout -b my-first-change
  2. +
  3. Edit a service file (e.g., change an image tag in stacks/echo/main.tf)
  4. +
  5. Commit and push:
    git add . && git commit -m "my first change" && git push -u origin my-first-change
  6. +
  7. Open a Pull Request on GitHub
  8. +
  9. Viktor reviews and merges
  10. +
  11. Woodpecker CI automatically applies the change to the cluster
  12. +
  13. Slack notification confirms it worked
  14. +
+
+
+ + diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/services/+page.svelte b/stacks/platform/modules/k8s-portal/files/src/routes/services/+page.svelte new file mode 100644 index 00000000..2d603dfb --- /dev/null +++ b/stacks/platform/modules/k8s-portal/files/src/routes/services/+page.svelte @@ -0,0 +1,52 @@ +
+

Service Catalog

+

70+ services running on the cluster. Here are the most commonly used:

+ +
+

Core Services

+ + + + + + +
ServiceURLDescription
Grafanagrafana.viktorbarzin.meMonitoring dashboards
Uptime Kumauptime.viktorbarzin.meService uptime monitoring
Authentikauthentik.viktorbarzin.meIdentity provider (SSO)
Woodpecker CIci.viktorbarzin.meCI/CD pipeline
+
+ +
+

User-Facing Services

+ + + + + + + + + +
ServiceURLDescription
Nextcloudnextcloud.viktorbarzin.meFile storage, calendar, contacts
Immichimmich.viktorbarzin.mePhoto library (Google Photos alternative)
Vaultwardenvault.viktorbarzin.mePassword manager
Paperless-ngxpdf.viktorbarzin.meDocument management
Navidromemusic.viktorbarzin.meMusic streaming
Tandoorrecipes.viktorbarzin.meRecipe manager
Linkwardenbookmarks.viktorbarzin.meBookmark manager
+
+ +
+

Developer Tools

+ + + + + + + +
ServiceURLDescription
Forgejoforgejo.viktorbarzin.meGit server (Gitea fork)
CyberChefcyberchef.viktorbarzin.meData transformation tool
Excalidrawdraw.viktorbarzin.meWhiteboard drawing
PrivateBinpaste.viktorbarzin.meEncrypted paste bin
JSON Crackjsoncrack.viktorbarzin.meJSON visualizer
+
+
+ + diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/setup/script/+server.ts b/stacks/platform/modules/k8s-portal/files/src/routes/setup/script/+server.ts index 7e512020..696ab6dc 100644 --- a/stacks/platform/modules/k8s-portal/files/src/routes/setup/script/+server.ts +++ b/stacks/platform/modules/k8s-portal/files/src/routes/setup/script/+server.ts @@ -4,7 +4,6 @@ import { readFileSync } from 'fs'; const CLUSTER_SERVER = 'https://10.0.20.100:6443'; const OIDC_ISSUER = 'https://authentik.viktorbarzin.me/application/o/kubernetes/'; const OIDC_CLIENT_ID = 'kubernetes'; -const PORTAL_URL = 'https://k8s-portal.viktorbarzin.me'; export const GET: RequestHandler = async ({ url }) => { const os = url.searchParams.get('os') || 'mac'; @@ -46,8 +45,6 @@ users: - --oidc-extra-scope=groups interactiveMode: IfAvailable`; - const escapedKubeconfig = kubeconfigContent.replace(/'/g, "'\\''"); - let script: string; if (os === 'linux') { @@ -98,7 +95,7 @@ fi # Write kubeconfig mkdir -p ~/.kube cat > ~/.kube/config-home << 'KUBECONFIG_EOF' -${escapedKubeconfig} +${kubeconfigContent} KUBECONFIG_EOF echo "[OK] Kubeconfig written to ~/.kube/config-home" @@ -152,7 +149,7 @@ fi # Write kubeconfig mkdir -p ~/.kube cat > ~/.kube/config-home << 'KUBECONFIG_EOF' -${escapedKubeconfig} +${kubeconfigContent} KUBECONFIG_EOF echo "[OK] Kubeconfig written to ~/.kube/config-home" diff --git a/stacks/platform/modules/k8s-portal/files/src/routes/troubleshooting/+page.svelte b/stacks/platform/modules/k8s-portal/files/src/routes/troubleshooting/+page.svelte new file mode 100644 index 00000000..17ac2e5a --- /dev/null +++ b/stacks/platform/modules/k8s-portal/files/src/routes/troubleshooting/+page.svelte @@ -0,0 +1,63 @@ +
+

Troubleshooting

+ +
+

"kubectl can't connect to the server"

+
    +
  1. Check your VPN: tailscale status — should show "connected"
  2. +
  3. Check KUBECONFIG: echo $KUBECONFIG — should be ~/.kube/config-home
  4. +
  5. Test connectivity: ping 10.0.20.100
  6. +
  7. If ping works but kubectl doesn't, re-run the setup script
  8. +
+
+ +
+

"Forbidden" or "Permission denied"

+

You may not have access to that namespace. Your access is scoped to specific namespaces.

+

Try: kubectl get namespaces to see which namespaces you can access.

+

Need access to another namespace? Ask Viktor.

+
+ +
+

"Pod is CrashLoopBackOff"

+
    +
  1. Check pod logs: kubectl logs -n <namespace> <pod-name> --tail=50
  2. +
  3. Check previous crash: kubectl logs -n <namespace> <pod-name> --previous
  4. +
  5. Check events: kubectl describe pod -n <namespace> <pod-name>
  6. +
  7. Common causes: OOMKilled (need more memory), bad config, database connection failure
  8. +
+
+ +
+

"PR CI failed"

+
    +
  1. Check the Woodpecker CI dashboard: ci.viktorbarzin.me
  2. +
  3. Read the build logs — the error is usually at the bottom
  4. +
  5. Fix the issue, commit, and push — CI will re-run
  6. +
+
+ +
+

"I need a new secret / database password"

+

Secrets are managed by Viktor in an encrypted file. You cannot add them yourself.

+
    +
  1. Comment on your PR: "Need DB password for <service>"
  2. +
  3. Viktor adds the secret and pushes to your branch
  4. +
  5. Reference it as var.<service>_db_password in your Terraform
  6. +
+
+ +
+

Still stuck?

+

Email Viktor at vbarzin@gmail.com or message on Slack.

+
+
+ + diff --git a/stacks/platform/modules/k8s-portal/main.tf b/stacks/platform/modules/k8s-portal/main.tf index 40217516..bab83dab 100644 --- a/stacks/platform/modules/k8s-portal/main.tf +++ b/stacks/platform/modules/k8s-portal/main.tf @@ -1,5 +1,9 @@ variable "tls_secret_name" {} variable "tier" { type = string } +variable "k8s_ca_cert" { + type = string + default = "" +} resource "kubernetes_namespace" "k8s_portal" { metadata { @@ -23,8 +27,7 @@ resource "kubernetes_config_map" "k8s_portal_config" { } data = { - # CA cert extracted from kubeconfig — will be populated with cluster CA cert - "ca.crt" = "" + "ca.crt" = var.k8s_ca_cert } } diff --git a/stacks/platform/modules/reverse_proxy/factory/main.tf b/stacks/platform/modules/reverse_proxy/factory/main.tf index cfbcf9c2..1af42844 100644 --- a/stacks/platform/modules/reverse_proxy/factory/main.tf +++ b/stacks/platform/modules/reverse_proxy/factory/main.tf @@ -17,7 +17,6 @@ variable "protected" { variable "ingress_path" { type = list(string) default = ["/"] - sensitive = true } variable "max_body_size" { type = string