Skip to content
Merged
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
8 changes: 5 additions & 3 deletions .ai/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -261,18 +261,20 @@ 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 |
|---------|--------------|-----------|
| `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 |
Comment thread
venkatamutyala marked this conversation as resolved.
| `<CaptainDomain />` | Inline prose / paragraph text (non-URL domain names) | MDX component — renders current domain as styled text |
| `<CaptainDomainPart segment="cluster" />` | Inline prose for the environment namespace (also accepts `tenant` / `tld`) | MDX component — renders that segment of the current domain |
| `<CaptainDomainLink to="https://sub.{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 `<CaptainDomain />` for bare domain names that are not clickable URLs. Files using this component **must** have a `.mdx` extension. Standard Docusaurus components like `<Tabs>` and `<TabItem>` work in `.md` files — only custom JSX components like `<CaptainDomain />` require `.mdx`.
3. **Prose text (clickable URLs)** — use `<CaptainDomainLink to="https://sub.{domain}/path" />` for any `https://` URL the reader should visit. The `to` prop uses `{domain}` as a placeholder. Optional `children` override the link text (e.g., `<CaptainDomainLink to="https://argocd.{domain}">ArgoCD dashboard</CaptainDomainLink>`). 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.
Expand Down
118 changes: 118 additions & 0 deletions docs/deploy-applications/access-cluster-kubectl.mdx
Original file line number Diff line number Diff line change
@@ -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 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: <CaptainDomainLink to="https://cluster-info.{domain}" />

:::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 <CaptainDomainLink to="https://cluster-info.{domain}" /> 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 <CaptainDomain /> it is <CaptainDomainPart segment="cluster" />:

```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 (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
Your token is cached after sign-in, so the browser step only happens occasionally — not on every command.
:::

To avoid typing `-n <namespace>` 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** | <code><CaptainDomain />-kubectl-reader</code> | View pods, logs, services, deployments, ingresses, and ExternalSecrets status. No access to secret values. |
| **Debugger** | <code><CaptainDomain />-kubectl-debugger</code> | Everything in Reader, plus: exec into pods, port-forward, attach, restart deployments, and delete stuck pods. |
| **Operator** | <code><CaptainDomain />-kubectl-operator</code> | 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 <verb> <resource> -n <namespace>`.

:::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 <namespace>` or a default namespace set.
:::

## Troubleshooting

### `Error from server (Forbidden)`

In order of likelihood:

1. Missing or wrong `-n <namespace>` — 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 <verb> <resource> -n <namespace>`.
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
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 or is refused 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.
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Visit <CaptainDomainLink to="https://hello-world-prod.apps.{domain}/?env=true" /
- `SECRET_MESSAGE=This value came from your Secret Store!` — injected via ExternalSecret

:::tip
If the secret doesn't appear immediately, give it a moment — the ExternalSecret controller refreshes every few seconds. You can also check the ExternalSecret resource status in the ArgoCD dashboard.
If the secret doesn't appear immediately, give it a moment — the ExternalSecret controller refreshes every few seconds. You can also check the ExternalSecret resource status in the ArgoCD dashboard, or with `kubectl get externalsecret` in your namespace (see [Access Your Cluster with kubectl](access-cluster-with-kubectl)).
:::

## Key concepts
Expand Down
1 change: 1 addition & 0 deletions docs/deploy-applications/hello-world.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,23 @@ html[data-theme="dark"] {
padding: 0.1em 0.3em;
}

/* When the inline domain sits inside a <code> chip (e.g. team names like
<code><CaptainDomain />-kubectl-reader</code>), 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);
border: none;
border-radius: 0;
padding: 0 0.15em;
}
Comment thread
Copilot marked this conversation as resolved.
Comment thread
Copilot marked this conversation as resolved.

/**
Captain Domain link (CaptainDomainLink component)
*/
Expand Down
7 changes: 6 additions & 1 deletion src/theme/CodeBlock/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment thread
venkatamutyala marked this conversation as resolved.
Comment thread
venkatamutyala marked this conversation as resolved.
return content;
}
Expand Down
2 changes: 1 addition & 1 deletion tests-e2e/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 .

Expand Down
55 changes: 55 additions & 0 deletions tests-e2e/captain-domain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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 <CaptainDomain /> in prose updates reactively', async ({ page }) => {
await gotoAndWait(page, PAGES.clusterDomains);

Expand Down
Loading