Skip to content

Commit 5d3d1bd

Browse files
committed
Permalink with kubeconfigID and path
1 parent 92bea39 commit 5d3d1bd

File tree

7 files changed

+172
-1
lines changed

7 files changed

+172
-1
lines changed

public/kubeconfig/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Ignore all kubeconfig files (they may contain sensitive data)
2+
*
3+
# But keep this .gitignore and the README
4+
!.gitignore
5+
!README.md
6+
!.gitkeep

public/kubeconfig/.gitkeep

Whitespace-only changes.

public/kubeconfig/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Local Kubeconfig Files for Development
2+
3+
This directory is used to store kubeconfig files for local development with the `kubeconfigID` permalink feature.
4+
5+
## Usage
6+
7+
1. Place your kubeconfig file in this directory **without a file extension**
8+
2. The filename becomes the `kubeconfigID` parameter value
9+
10+
### Example
11+
12+
1. Copy your kubeconfig:
13+
```bash
14+
cp ~/.kube/config public/kubeconfig/myconfig
15+
```
16+
17+
2. Access Busola with the permalink:
18+
```
19+
http://localhost:8080/?kubeconfigID=myconfig&path=/namespaces/default/pods
20+
```
21+
22+
## URL Format
23+
24+
```
25+
/?kubeconfigID=<filename>&path=<path>
26+
```
27+
28+
### Parameters
29+
30+
| Parameter | Description |
31+
|-----------|-------------|
32+
| `kubeconfigID` | The filename (without extension) of the kubeconfig file in this directory |
33+
| `path` | (Optional) The path to navigate to after loading the kubeconfig |
34+
35+
### Path Examples
36+
37+
| Path | Description |
38+
|------|-------------|
39+
| `/namespaces/default/pods` | Pods in default namespace |
40+
| `/namespaces/kube-system/deployments` | Deployments in kube-system |
41+
| `/nodes` | Cluster nodes |
42+
| `/overview` | Cluster overview |
43+
44+
## Security Note
45+
46+
⚠️ **Do not commit kubeconfig files to the repository!**
47+
48+
The `.gitignore` in this directory is configured to ignore all files except documentation.
49+
Kubeconfig files may contain sensitive credentials like tokens or certificates.
50+
51+
## Production Configuration
52+
53+
In production, the `kubeconfigUrl` is configured via the `KUBECONFIG_ID` feature flag in `defaultConfig.yaml`:
54+
55+
```yaml
56+
KUBECONFIG_ID:
57+
isEnabled: true
58+
config:
59+
kubeconfigUrl: https://your-kubeconfig-service.example.com/kubeconfig
60+
```
61+
62+
See [Feature Flags Documentation](../../docs/user/technical-reference/feature-flags.md) for more details.

src/components/Clusters/shared.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { SetStateAction, useSetAtom } from 'jotai';
1515
import { removePreviousPath } from 'state/useAfterInitHook';
1616
import { ManualKubeConfigIdType } from 'state/manualKubeConfigIdAtom';
1717
import { parseOIDCparams } from 'components/Clusters/components/oidc-params';
18+
import { getIntendedPath, clearIntendedPath } from 'state/intendedPathAtom';
1819

1920
export type Users = Array<{
2021
name: string;
@@ -29,7 +30,17 @@ function addCurrentCluster(
2930

3031
if (clustersInfo.currentCluster?.name !== params?.name) removePreviousPath();
3132

32-
if (params.currentContext.namespace) {
33+
// Check for intended path from kubeconfigID permalink
34+
// If there's an intended path, navigate to cluster overview first.
35+
// The actual navigation to the intended path will happen in useAfterInitHook
36+
// after auth is fully set up.
37+
const intendedPath = getIntendedPath();
38+
if (intendedPath?.path) {
39+
// Navigate to cluster overview - useAfterInitHook will handle the intended path
40+
clustersInfo.navigate(
41+
`/cluster/${encodeURIComponent(params.contextName)}/overview`,
42+
);
43+
} else if (params.currentContext.namespace) {
3344
clustersInfo.navigate(
3445
`/cluster/${encodeURIComponent(params.contextName)}/namespaces/${
3546
params.currentContext.namespace

src/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { initReactI18next } from 'react-i18next';
55
import { createBrowserRouter, RouterProvider } from 'react-router';
66
import i18nextBackend from 'i18next-http-backend';
77
import { savePreviousPath } from 'state/useAfterInitHook';
8+
import { initIntendedPathFromUrl } from 'state/intendedPathAtom';
89

910
import App from './components/App/App';
1011
import { Spinner } from 'shared/components/Spinner/Spinner';
@@ -62,6 +63,10 @@ i18next
6263

6364
savePreviousPath();
6465

66+
// Initialize intended path from URL parameters (for kubeconfigID permalinks)
67+
// This must be called early before any redirects happen
68+
initIntendedPathFromUrl();
69+
6570
const container = document.getElementById('root');
6671
const root = createRoot(container!);
6772
const isDevMode =

src/state/intendedPathAtom.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Intended Path Storage Utility
3+
*
4+
* This module handles storing and retrieving the intended navigation path
5+
* for permalinks with kubeconfigID. The path is stored in sessionStorage
6+
* to survive OIDC redirects and page reloads.
7+
*
8+
* URL Format: /?kubeconfigID=abc123&path=/namespaces/default/pods
9+
*/
10+
11+
const INTENDED_PATH_KEY = 'busola.intended-path';
12+
13+
export interface IntendedPath {
14+
path: string;
15+
kubeconfigId?: string;
16+
timestamp: number;
17+
}
18+
19+
/**
20+
* Save the intended path to sessionStorage.
21+
* Only saves if the path is valid (not root or clusters page).
22+
*/
23+
export function saveIntendedPath(path: string, kubeconfigId?: string): void {
24+
if (path && path !== '/' && path !== '/clusters') {
25+
const data: IntendedPath = {
26+
path,
27+
kubeconfigId,
28+
timestamp: Date.now(),
29+
};
30+
sessionStorage.setItem(INTENDED_PATH_KEY, JSON.stringify(data));
31+
}
32+
}
33+
34+
/**
35+
* Get the intended path from sessionStorage.
36+
* Returns null if no path is stored or if the path has expired (5 minutes).
37+
*/
38+
export function getIntendedPath(): IntendedPath | null {
39+
const data = sessionStorage.getItem(INTENDED_PATH_KEY);
40+
if (!data) return null;
41+
42+
try {
43+
const parsed: IntendedPath = JSON.parse(data);
44+
// Expire after 5 minutes to avoid stale redirects
45+
if (Date.now() - parsed.timestamp > 5 * 60 * 1000) {
46+
clearIntendedPath();
47+
return null;
48+
}
49+
return parsed;
50+
} catch {
51+
clearIntendedPath();
52+
return null;
53+
}
54+
}
55+
56+
/**
57+
* Clear the intended path from sessionStorage.
58+
*/
59+
export function clearIntendedPath(): void {
60+
sessionStorage.removeItem(INTENDED_PATH_KEY);
61+
}
62+
63+
/**
64+
* Initialize intended path from URL parameters.
65+
* Should be called early in the app initialization.
66+
*/
67+
export function initIntendedPathFromUrl(): void {
68+
const params = new URLSearchParams(window.location.search);
69+
const kubeconfigId = params.get('kubeconfigID');
70+
const path = params.get('path');
71+
72+
if (kubeconfigId && path) {
73+
saveIntendedPath(path, kubeconfigId);
74+
}
75+
}

src/state/useAfterInitHook.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useNavigate, useSearchParams } from 'react-router';
44
import { useAtomValue } from 'jotai';
55
import { authDataAtom } from './authDataAtom';
66
import { clusterAtom } from './clusterAtom';
7+
import { getIntendedPath, clearIntendedPath } from './intendedPathAtom';
78

89
const PREVIOUS_PATHNAME_KEY = 'busola.previous-pathname';
910

@@ -84,6 +85,17 @@ export function useAfterInitHook(handledKubeconfigId: KubeconfigIdHandleState) {
8485
}
8586

8687
initDone.current = true;
88+
89+
// Handle intended path from kubeconfigID permalinks
90+
// This happens after auth is ready to ensure API calls work
91+
const intendedPath = getIntendedPath();
92+
if (intendedPath?.path && cluster) {
93+
const fullPath = `/cluster/${encodeURIComponent(cluster.name)}${intendedPath.path}`;
94+
navigate(fullPath);
95+
clearIntendedPath();
96+
return;
97+
}
98+
8799
const previousPath = getPreviousPath();
88100

89101
if (

0 commit comments

Comments
 (0)