From 1164f33cbde133090fcc318e8af5742e63e01738 Mon Sep 17 00:00:00 2001 From: Venkat Date: Sat, 13 Jun 2026 00:45:24 +0000 Subject: [PATCH 1/4] docs: add developer guide for cluster access with kubectl #patch Adds an end-user guide covering kubectl/krew/oidc-login prerequisites, creating the kubeconfig from the cluster-info page, GitHub device-code sign-in, the three access tiers (reader/debugger/operator), and namespace-scoped access with troubleshooting. Adds a CAPTAIN_NAMESPACE sentinel to the swizzled CodeBlock so commands render with the reader's environment namespace (the first label of the Captain Domain), and a CSS rule so an inline CaptainDomain inside a code chip blends into a single monospace token. Supersedes the operator-focused draft in PR #477: the Traefik exposure and RBAC manifests now live in the per-cluster GitOps repos, and the hand-built kubeconfig script is replaced by the cluster-info kubeconfig. --- .ai/reference.md | 4 +- .../access-cluster-kubectl.mdx | 118 ++++++++++++++++++ docs/deploy-applications/hello-world.mdx | 1 + sidebars.js | 1 + src/css/custom.css | 15 +++ src/theme/CodeBlock/index.tsx | 7 +- 6 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 docs/deploy-applications/access-cluster-kubectl.mdx diff --git a/.ai/reference.md b/.ai/reference.md index edc8b9e80..3a4c1f8bc 100644 --- a/.ai/reference.md +++ b/.ai/reference.md @@ -266,13 +266,15 @@ The site replaces domain references dynamically so readers see their own cluster | Pattern | Where to use | Handled by | |---------|--------------|-----------| | `CAPTAIN_DOMAIN` | Inside code fences (` ``` `) | Swizzled `CodeBlock` component — replaces at render time | +| `CAPTAIN_NAMESPACE` | Inside code fences, where a command needs the reader's environment namespace (first label of the captain domain, e.g. `nonprod`) | Swizzled `CodeBlock` component — replaces at render time | | `` | Inline prose / paragraph text (non-URL domain names) | MDX component — renders current domain as styled text | +| `` | Inline prose for the environment namespace (also accepts `tenant` / `tld`) | MDX component — renders that segment of the current domain | | `` | `https://` URLs in prose that readers should visit | `CaptainDomainLink` component — clickable link when domain is customized, styled text with tooltip when default | | `{{ .Values.captain_domain }}` | Helm template YAML inside code fences | Not replaced — displayed as-is (real Helm expression) | ### Rules -1. **Code fences** — write `CAPTAIN_DOMAIN` as a literal sentinel. The swizzled CodeBlock replaces every occurrence with the reader's domain. +1. **Code fences** — write `CAPTAIN_DOMAIN` as a literal sentinel. The swizzled CodeBlock replaces every occurrence with the reader's domain. Use `CAPTAIN_NAMESPACE` the same way for the environment namespace (the first label of the captain domain, e.g. `kubectl get pods -n CAPTAIN_NAMESPACE`). 2. **Prose text (non-URL domain names)** — use `` for bare domain names that are not clickable URLs. Files using this component **must** have a `.mdx` extension. Standard Docusaurus components like `` and `` work in `.md` files — only custom JSX components like `` require `.mdx`. 3. **Prose text (clickable URLs)** — use `` for any `https://` URL the reader should visit. The `to` prop uses `{domain}` as a placeholder. Optional `children` override the link text (e.g., `ArgoCD dashboard`). When the reader has set their domain, it renders as a clickable link opening in a new tab. When using the default domain, it renders as styled text with a tooltip prompting them to set their domain. Requires `.mdx` extension. 4. **Helm YAML in code fences** — use `{{ .Values.captain_domain }}`. This is the actual Helm expression and is intentionally left as-is. diff --git a/docs/deploy-applications/access-cluster-kubectl.mdx b/docs/deploy-applications/access-cluster-kubectl.mdx new file mode 100644 index 000000000..96fa6fc4c --- /dev/null +++ b/docs/deploy-applications/access-cluster-kubectl.mdx @@ -0,0 +1,118 @@ +--- +id: access-cluster-with-kubectl +title: Access Your Cluster with kubectl +type: tutorial +--- + +# Access Your Cluster with kubectl + +You can connect to your GlueOps environment with `kubectl` using your GitHub identity — no certificates or passwords to manage. Sign-in happens through your browser, and access is scoped to your team's environment namespace at one of three permission tiers. + +## Prerequisites + +- [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) installed on your machine. +- [krew](https://krew.sigs.k8s.io/docs/user-guide/setup/install/) — the plugin manager for kubectl. +- [oidc-login](https://github.com/int128/kubelogin#setup) — the kubectl plugin that handles the browser sign-in. +- Membership in one of your organization's kubectl GitHub teams (see [Access tiers](#access-tiers) below). Your Platform Administrator manages team membership. +- Your kubeconfig, available from your cluster information page: + +:::tip +If the domain above doesn't look right, update your **Captain Domain** in the top navigation bar. +::: + +## 1. Create your kubeconfig + +Open your cluster information page at and copy the kubeconfig shown there. It comes with the cluster's certificate authority and sign-in configuration already embedded — no editing required. + +Paste it into kubectl's default config file: + +```bash +mkdir -p ~/.kube +# paste the copied kubeconfig into this file with your editor: +nano ~/.kube/config +``` + +kubectl picks this file up automatically — no environment variables needed. + +:::caution +If you already have a `~/.kube/config` for other clusters, don't overwrite it — merge the new cluster, context, and user entries into your existing file instead. +::: + +## 2. Connect and sign in + +Run any command against your environment namespace — that's the first part of your Captain Domain, so for it is : + +```bash +kubectl get pods -n CAPTAIN_NAMESPACE +``` + +The first time, kubectl starts a device sign-in flow: + +1. Your terminal prints a verification URL and a one-time code. +2. Open the URL in your browser, enter the code, and sign in with GitHub. +3. Back in your terminal, the command completes and prints your pods. + +:::note +Your token is cached after sign-in, so the browser step only happens occasionally — not on every command. +::: + +To avoid typing `-n ` on every command, set it as your default once: + +```bash +kubectl config set-context --current --namespace=CAPTAIN_NAMESPACE +``` + +## Access tiers + +Your permissions depend on which GitHub team you belong to. There are three tiers, each including everything from the tier above it: + +| Tier | GitHub team | What you can do | +|---|---|---| +| **Reader** | -kubectl-reader | View pods, logs, services, deployments, ingresses, and ExternalSecrets status. No access to secret values. | +| **Debugger** | -kubectl-debugger | Everything in Reader, plus: exec into pods, port-forward, attach, restart deployments, and delete stuck pods. | +| **Operator** | -kubectl-operator | Everything in Debugger, plus: delete any resource in your namespace. | + +To see which groups you signed in with, run `kubectl auth whoami`. To check whether your tier allows a specific action, run `kubectl auth can-i -n `. + +:::note +The platform is GitOps-managed: you can't create or apply resources with kubectl. Deleting a resource causes ArgoCD to recreate it from your deployment-configurations repository — that's the intended way to force a clean resync. +::: + +## Your access is namespace-scoped + +Every tier grants access **only within your environment namespace**. Commands that list across the cluster will be denied: + +:::caution +`kubectl get namespaces`, `kubectl get pods -A`, and node-level views return `Forbidden`. This is by design, not an error — always work within your namespace with `-n ` or a default namespace set. +::: + +## Troubleshooting + +### `Error from server (Forbidden)` + +In order of likelihood: + +1. Missing or wrong `-n ` — you only have access to your own environment namespace. +2. The action needs a higher tier — check the [Access tiers](#access-tiers) table and verify with `kubectl auth can-i -n `. +3. You're not in a kubectl GitHub team yet — contact your Platform Administrator. + +### Device code expired or browser window closed + +Just rerun your kubectl command — a fresh code is issued each time. + +### Signed in but permissions seem stale + +Tokens are cached locally. After your team membership changes, clear the cache and sign in again: + +```bash +rm -rf ~/.kube/cache/oidc-login +``` + +### Connection times out before any sign-in prompt + +API access is restricted to an IP allowlist. If you're connecting from a new network or VPN and the connection hangs without ever showing a sign-in prompt, ask your Platform Administrator to add your IP address. + +## Next steps + +- [Deploy Your First App](deploy-first-app) — deploy something to inspect with your new access. +- [Add Secrets](manage-environment-secrets) — manage sensitive configuration the supported way. diff --git a/docs/deploy-applications/hello-world.mdx b/docs/deploy-applications/hello-world.mdx index df8feaf09..6b36ad476 100644 --- a/docs/deploy-applications/hello-world.mdx +++ b/docs/deploy-applications/hello-world.mdx @@ -116,4 +116,5 @@ Look for `GREETING_MESSAGE=Hello, World!` in the output. ## Next steps - [Add Secrets](manage-environment-secrets) — Pull sensitive configuration from your secret store instead of hardcoding values. +- [Access Your Cluster with kubectl](access-cluster-with-kubectl) — Inspect pods and logs for your deployed app. - [Traefik Ingress & Routing](/traefik-ingress) — Explore advanced routing patterns: path-based routing, middleware, rate limiting, and more. diff --git a/sidebars.js b/sidebars.js index 0850e7958..a10bf20b9 100644 --- a/sidebars.js +++ b/sidebars.js @@ -27,6 +27,7 @@ const sidebars = { items: [ "deploy-applications/deploy-first-app", "deploy-applications/manage-environment-secrets", + "deploy-applications/access-cluster-with-kubectl", "deploy-applications/ingress/glueops-ingress-and-loadbalancer-customizations", { type: "category", diff --git a/src/css/custom.css b/src/css/custom.css index 29881438a..2453571f1 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -189,6 +189,21 @@ html[data-theme="dark"] { padding: 0.1em 0.3em; } +/* When the inline domain sits inside a chip (e.g. team names like + -kubectl-reader), blend it into the chip + instead of rendering a chip-within-a-chip. The custom-domain highlight + still applies via .captain-domain-custom. */ +code .captain-domain-inline { + background: transparent; + font-size: 1em; + padding: 0; +} + +code .captain-domain-custom { + background: rgba(233, 171, 23, 0.12); + padding: 0 0.15em; +} + /** Captain Domain link (CaptainDomainLink component) */ diff --git a/src/theme/CodeBlock/index.tsx b/src/theme/CodeBlock/index.tsx index ec01c29b9..5fc13df7d 100644 --- a/src/theme/CodeBlock/index.tsx +++ b/src/theme/CodeBlock/index.tsx @@ -3,10 +3,15 @@ import OriginalCodeBlock from '@theme-original/CodeBlock'; import { useCaptainDomain } from '@site/src/contexts/CaptainDomainContext'; const SENTINEL = 'CAPTAIN_DOMAIN'; +// First label of the captain domain is the environment namespace +// (e.g. "nonprod" in nonprod.tenant.onglueops.com). +const NAMESPACE_SENTINEL = 'CAPTAIN_NAMESPACE'; function replaceDomain(content: unknown, captainDomain: string): unknown { if (typeof content === 'string') { - return content.replaceAll(SENTINEL, captainDomain); + return content + .replaceAll(NAMESPACE_SENTINEL, captainDomain.split('.')[0] || '') + .replaceAll(SENTINEL, captainDomain); } return content; } From 0126cf9ebdc562e865657b0d578421c5be185216 Mon Sep 17 00:00:00 2001 From: Venkat Date: Sat, 13 Jun 2026 02:44:08 +0000 Subject: [PATCH 2/4] docs: address review feedback on cluster access guide #patch - clarify oidc-login is installed via krew and needs ~/.krew/bin on PATH - soften device-code wording (browser may open automatically) - use 'kubectl oidc-login clean' instead of rm -rf for token cache - note connection may be refused (not only hang) when IP not allowlisted --- docs/deploy-applications/access-cluster-kubectl.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/deploy-applications/access-cluster-kubectl.mdx b/docs/deploy-applications/access-cluster-kubectl.mdx index 96fa6fc4c..b4f5136d4 100644 --- a/docs/deploy-applications/access-cluster-kubectl.mdx +++ b/docs/deploy-applications/access-cluster-kubectl.mdx @@ -12,7 +12,7 @@ You can connect to your GlueOps environment with `kubectl` using your GitHub ide - [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) installed on your machine. - [krew](https://krew.sigs.k8s.io/docs/user-guide/setup/install/) — the plugin manager for kubectl. -- [oidc-login](https://github.com/int128/kubelogin#setup) — the kubectl plugin that handles the browser sign-in. +- [oidc-login](https://github.com/int128/kubelogin#setup) — the kubectl credential plugin that handles the browser sign-in. Install it with krew (`kubectl krew install oidc-login`), and make sure `~/.krew/bin` is on your `PATH` so kubectl can find it. - Membership in one of your organization's kubectl GitHub teams (see [Access tiers](#access-tiers) below). Your Platform Administrator manages team membership. - Your kubeconfig, available from your cluster information page: @@ -48,8 +48,8 @@ kubectl get pods -n CAPTAIN_NAMESPACE The first time, kubectl starts a device sign-in flow: -1. Your terminal prints a verification URL and a one-time code. -2. Open the URL in your browser, enter the code, and sign in with GitHub. +1. Your terminal prints a verification URL and a one-time code (your browser may open automatically). +2. In the browser, enter the code if prompted and sign in with GitHub. 3. Back in your terminal, the command completes and prints your pods. :::note @@ -105,12 +105,12 @@ Just rerun your kubectl command — a fresh code is issued each time. Tokens are cached locally. After your team membership changes, clear the cache and sign in again: ```bash -rm -rf ~/.kube/cache/oidc-login +kubectl oidc-login clean ``` ### Connection times out before any sign-in prompt -API access is restricted to an IP allowlist. If you're connecting from a new network or VPN and the connection hangs without ever showing a sign-in prompt, ask your Platform Administrator to add your IP address. +API access is restricted to an IP allowlist. If you're connecting from a new network or VPN and the connection hangs or is refused without ever showing a sign-in prompt, ask your Platform Administrator to add your IP address. ## Next steps From ee18d14ad703e8520a9f653236c5c550304299b4 Mon Sep 17 00:00:00 2001 From: Venkat Date: Sat, 13 Jun 2026 02:51:55 +0000 Subject: [PATCH 3/4] docs: align kubectl-access reality across docs #patch - correct .ai/reference.md Verify convention: developers now have namespace-scoped kubectl access, so scoped kubectl verification is acceptable (was: 'platform users do not have kubectl access') - cross-link the kubectl access guide from the ExternalSecret tip --- .ai/reference.md | 2 +- docs/deploy-applications/hello-world-adding-configurations.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ai/reference.md b/.ai/reference.md index 3a4c1f8bc..d9321b2e1 100644 --- a/.ai/reference.md +++ b/.ai/reference.md @@ -216,7 +216,7 @@ All Traefik guide pages follow this structure: 3. **Prerequisites** — `base/base-values.yaml` code block 4. **Configuration** — tabbed `customResources` (list) vs `customResourcesMap` (map) examples 5. **What Gets Created** or **How It Works** — table or bullet list -6. **Verify** — `curl` commands (do not use `kubectl` — platform users do not have kubectl access; for certificate or TCP apps, mention the ArgoCD dashboard for resource status) +6. **Verify** — `curl` commands are the primary check. `kubectl` is also acceptable when scoped to the reader's own environment namespace (e.g. `kubectl get pods -n CAPTAIN_NAMESPACE`), since developers now have namespace-scoped cluster access (see the "Access Your Cluster with kubectl" guide). Avoid cluster-wide commands (`-A`, `get namespaces`, node views) — RBAC forbids them. For certificate or TCP apps, the ArgoCD dashboard is also a good source for resource status. 7. **Key Points** — bullet list of important takeaways 8. **Admonitions** — `:::info`, `:::caution`, `:::warning` at the end diff --git a/docs/deploy-applications/hello-world-adding-configurations.mdx b/docs/deploy-applications/hello-world-adding-configurations.mdx index 3b389d225..c918c8ef3 100644 --- a/docs/deploy-applications/hello-world-adding-configurations.mdx +++ b/docs/deploy-applications/hello-world-adding-configurations.mdx @@ -111,7 +111,7 @@ Visit Date: Sat, 13 Jun 2026 03:32:29 +0000 Subject: [PATCH 4/4] docs: address PR review feedback (Copilot) #patch - reference.md: drop stale 'three patterns' count (table lists more) - custom.css: fully blend custom-domain token inside code chips (reset border/radius, not just background) - e2e: add CAPTAIN_NAMESPACE coverage on the new access page (default + custom domain), closing the regression gap - e2e: fix pre-existing playwright version mismatch (@playwright/test 1.49.1 vs base image 1.58.2) that broke the entire suite --- .ai/reference.md | 2 +- src/css/custom.css | 2 ++ tests-e2e/Dockerfile | 2 +- tests-e2e/captain-domain.spec.ts | 55 ++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/.ai/reference.md b/.ai/reference.md index d9321b2e1..65bec0b57 100644 --- a/.ai/reference.md +++ b/.ai/reference.md @@ -261,7 +261,7 @@ curl https://{{ include "app.name" . }}.apps.{{ .Values.captain_domain }} ## Captain Domain — Dynamic Domain Replacement -The site replaces domain references dynamically so readers see their own cluster domain. There are **three patterns** depending on context: +The site replaces domain references dynamically so readers see their own cluster domain. Choose the pattern that fits the context: | Pattern | Where to use | Handled by | |---------|--------------|-----------| diff --git a/src/css/custom.css b/src/css/custom.css index 2453571f1..49af3e7aa 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -201,6 +201,8 @@ code .captain-domain-inline { code .captain-domain-custom { background: rgba(233, 171, 23, 0.12); + border: none; + border-radius: 0; padding: 0 0.15em; } diff --git a/tests-e2e/Dockerfile b/tests-e2e/Dockerfile index bba82c61d..c25da694d 100644 --- a/tests-e2e/Dockerfile +++ b/tests-e2e/Dockerfile @@ -1,7 +1,7 @@ FROM mcr.microsoft.com/playwright:v1.58.2-noble@sha256:6446946a1d9fd62d9ae501312a2d76a43ee688542b21622056a372959b65d63d WORKDIR /tests -RUN npm init -y && npm install @playwright/test@1.49.1 +RUN npm init -y && npm install @playwright/test@1.58.2 COPY captain-domain.spec.ts playwright.config.ts . diff --git a/tests-e2e/captain-domain.spec.ts b/tests-e2e/captain-domain.spec.ts index 6b8f466f1..9134dd64c 100644 --- a/tests-e2e/captain-domain.spec.ts +++ b/tests-e2e/captain-domain.spec.ts @@ -10,8 +10,11 @@ const PAGES = { tlsRedirect: '/deploy-applications/traefik/traefik-tls-redirect', clusterDomains: '/glueops-captain-domain', introduction: '/introduction', + accessCluster: '/deploy-applications/access-cluster-with-kubectl', }; +const DEFAULT_NAMESPACE = DEFAULT_DOMAIN.split('.')[0]; // first label, e.g. "nonprod" + async function gotoAndWait(page, path: string) { await page.goto(`${BASE_URL}${path}`, { waitUntil: 'networkidle', timeout: 60000 }); await page.waitForSelector('#captain-domain-input', { timeout: 30000 }); @@ -227,6 +230,58 @@ test.describe('Captain Domain Feature', () => { } }); + test('CAPTAIN_NAMESPACE sentinel renders the namespace, not the raw token', async ({ page }) => { + await gotoAndWait(page, PAGES.accessCluster); + await waitForCodeBlocks(page); + + // No raw CAPTAIN_NAMESPACE (or CAPTAIN_DOMAIN) should remain in any code block + await page.waitForFunction( + () => { + const pres = document.querySelectorAll('pre'); + for (const pre of pres) { + const text = pre.textContent || ''; + if (text.includes('CAPTAIN_NAMESPACE') || text.includes('CAPTAIN_DOMAIN')) return false; + } + return pres.length > 0; + }, + { timeout: 15000 } + ); + + const allText = await page.locator('pre').allTextContents(); + for (const text of allText) { + expect(text, 'Raw CAPTAIN_NAMESPACE sentinel found').not.toContain('CAPTAIN_NAMESPACE'); + } + // The default namespace (first label of the domain) should appear, e.g. "-n nonprod" + const hasNamespace = allText.some(t => t.includes(`-n ${DEFAULT_NAMESPACE}`)); + expect(hasNamespace, `Expected "-n ${DEFAULT_NAMESPACE}" in a code block`).toBe(true); + }); + + test('CAPTAIN_NAMESPACE updates with a custom domain', async ({ page }) => { + await gotoAndWait(page, PAGES.accessCluster); + await waitForCodeBlocks(page); + + const input = page.locator('#captain-domain-input'); + await input.click(); + await input.fill('staging.acme.onglueops.rocks'); + await input.press('Enter'); + + // Namespace is the first label -> "staging" + await page.waitForFunction( + () => { + const pres = document.querySelectorAll('pre'); + for (const pre of pres) { + if ((pre.textContent || '').includes('-n staging')) return true; + } + return false; + }, + { timeout: 10000 } + ); + + const allText = await page.locator('pre').allTextContents(); + expect(allText.some(t => t.includes('-n staging'))).toBe(true); + expect(allText.some(t => t.includes('-n nonprod'))).toBe(false); + }); + test('inline in prose updates reactively', async ({ page }) => { await gotoAndWait(page, PAGES.clusterDomains);