Skip to main content

Working with Kubernetes Data

This tutorial teaches you how to fetch and display Kubernetes data in your plugin. You'll learn three different approaches: using cluster context, built-in resource classes, and direct API calls.


Table of Contents

  1. Introduction
  2. Understanding Cluster Context
  3. Using Built-in Resource Classes
  4. Fetching Single Resources
  5. Direct API Calls with ApiProxy
  6. Troubleshooting
  7. What's Next
  8. Quick Reference

Introduction

In Tutorial 3, you created pages with static content. Real plugins need to display live Kubernetes data like pods, deployments, nodes, etc.

This tutorial covers three ways to fetch Kubernetes data:

ApproachBest ForExample
getCluster()Getting current cluster contextDisplay cluster name
K8s.ResourceClassesStandard Kubernetes resourcesList namespaces, nodes
ApiProxy.request()Custom endpoints, raw API callsFetch API server version

What You'll Build

By the end of this tutorial, your plugin will:

  • Display the current cluster name
  • Show a list of namespaces
  • List namespaces as clickable buttons
  • Display namespace details when clicked
  • Fetch and display API server version information

Prerequisites

Before starting, ensure you have:

Time to complete: ~25 minutes


Understanding Cluster Context

When a user navigates to a cluster-specific page, your plugin needs to know which cluster they're viewing. Headlamp provides the getCluster() utility for this.

Display the Current Cluster

Let's update your Overview page to show the current cluster name.

Open src/index.tsx and update it:

import {
registerRoute,
registerSidebarEntry,
} from '@kinvolk/headlamp-plugin/lib';
import { getCluster } from '@kinvolk/headlamp-plugin/lib/Utils';
import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { Typography, Box } from '@mui/material';

function OverviewPage() {
const cluster = getCluster();

return (
<SectionBox title="Plugin Overview">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography>Current cluster:</Typography>
{cluster ? <>{cluster}</> : <>{'N/A'}</>}
</Box>
</SectionBox>
);
}

// Register the route (cluster view)
registerRoute({
path: '/my-plugin',
sidebar: 'my-plugin-overview',
component: OverviewPage,
exact: true,
});

// Register sidebar entries
registerSidebarEntry({
name: 'my-plugin',
label: 'My Plugin',
icon: 'mdi:new-box',
url: '/my-plugin',
});

registerSidebarEntry({
parent: 'my-plugin',
name: 'my-plugin-overview',
label: 'Overview',
url: '/my-plugin',
});

Test It

  1. Save the file
  2. Navigate to a cluster in Headlamp
  3. Click My PluginOverview in the sidebar
  4. You should see the cluster name displayed!

Screenshot of the Overview page showing the cluster name with a red rectangle highlighting the cluster name text

What's happening?

CodePurpose
getCluster()Returns the current cluster name from the URL (e.g., minikube)
Returns nullWhen not in a cluster context (home view)

Note: getCluster() reads the cluster from the URL path (/c/:cluster/...). It only works on cluster-specific pages where useClusterURL: true (the default).


Using Built-in Resource Classes

Now let's fetch actual Kubernetes resources. Headlamp provides K8s.ResourceClasses with built-in hooks for all standard Kubernetes resources including Pod, Namespace, Node, Deployment, Service, ConfigMap, Secret, and many more.

📚 Full list: See ResourceClasses API Reference for all available resource classes.

Each class provides two hooks:

  • useList() — Fetch multiple resources
  • useGet(name, namespace?) — Fetch a single resource

List Namespaces

Let's display all namespaces in the cluster. Update your src/index.tsx:

import {
K8s,
registerRoute,
registerSidebarEntry,
} from '@kinvolk/headlamp-plugin/lib';
import { getCluster } from '@kinvolk/headlamp-plugin/lib/Utils';
import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import {
Typography,
Box,
CircularProgress,
List,
ListItem,
ListItemText,
} from '@mui/material';

function OverviewPage() {
const cluster = getCluster();

// Fetch all namespaces
const [namespaces, namespacesError] = K8s.ResourceClasses.Namespace.useList();

// Loading state
if (!namespaces && !namespacesError) {
return (
<SectionBox title="Plugin Overview">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<CircularProgress size={20} />
<Typography>Loading namespaces...</Typography>
</Box>
</SectionBox>
);
}

// Error state
if (namespacesError) {
return (
<SectionBox title="Plugin Overview">
<Typography color="error">
Error loading namespaces: {namespacesError.message}
</Typography>
</SectionBox>
);
}

return (
<SectionBox title="Plugin Overview">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<Typography>Current cluster:</Typography>
{cluster ? <>{cluster}</> : <>{'N/A'}</>}
</Box>

<Typography variant="h6" sx={{ mb: 1 }}>
Namespaces ({namespaces?.length || 0})
</Typography>

<List dense>
{namespaces?.slice(0, 5).map(ns => (
<ListItem key={ns.metadata.name}>
<ListItemText
primary={ns.metadata.name}
secondary={`Created: ${new Date(
ns.metadata.creationTimestamp
).toLocaleDateString()}`}
/>
</ListItem>
))}
{namespaces && namespaces.length > 5 && (
<ListItem>
<ListItemText
secondary={`... and ${namespaces.length - 5} more`}
/>
</ListItem>
)}
</List>
</SectionBox>
);
}

registerRoute({
path: '/my-plugin',
sidebar: 'my-plugin-overview',
component: OverviewPage,
exact: true,
});

registerSidebarEntry({
name: 'my-plugin',
label: 'My Plugin',
icon: 'mdi:new-box',
url: '/my-plugin',
});

registerSidebarEntry({
parent: 'my-plugin',
name: 'my-plugin-overview',
label: 'Overview',
url: '/my-plugin',
});

Test It

Save and navigate to your plugin page. You should see:

  • The cluster name
  • A count of namespaces
  • The first 5 namespaces listed

Screenshot of the Overview page showing a list of namespaces with red rectangle highlighting the namespace list

Understanding the useList Hook

const [namespaces, error] = K8s.ResourceClasses.Namespace.useList();
Return ValueTypeDescription
namespacesNamespace[] | nullArray of namespace objects, or null while loading
errorError | nullError object if the request failed

The Three States:

// Loading: data is null, no error
if (!namespaces && !namespacesError) {
return <Loading />;
}

// Error: request failed
if (namespacesError) {
return <Error message={namespacesError.message} />;
}

// Success: data is available
return <DisplayData namespaces={namespaces} />;

Fetching Single Resources

Sometimes you need to fetch a specific resource by name. Use useGet() for this.

Add Namespace Details

Let's enhance our OverviewPage to show details when a namespace is clicked. Update your src/index.tsx:

import { useState } from 'react';
import {
K8s,
registerRoute,
registerSidebarEntry,
} from '@kinvolk/headlamp-plugin/lib';
import { getCluster } from '@kinvolk/headlamp-plugin/lib/Utils';
import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import {
Typography,
Box,
CircularProgress,
Button,
Paper,
} from '@mui/material';

// Component to show details of a selected namespace
function NamespaceDetail({ namespace }: { namespace: string }) {
// Fetch a single namespace by name using useGet()
const [ns, nsError] = K8s.ResourceClasses.Namespace.useGet(namespace);

if (!ns && !nsError) {
return <CircularProgress size={20} />;
}

if (nsError) {
return <Typography color="error">Namespace not found</Typography>;
}

return (
<Paper variant="outlined" sx={{ p: 2 }}>
<Typography variant="h6">{ns.metadata.name}</Typography>
<Typography color="text.secondary">
Status: {ns.status?.phase}
</Typography>
<Typography color="text.secondary">
Created: {new Date(ns.metadata.creationTimestamp).toLocaleString()}
</Typography>
<Typography color="text.secondary">
Labels: {Object.keys(ns.metadata.labels || {}).length}
</Typography>
</Paper>
);
}

function OverviewPage() {
const cluster = getCluster();
const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null);

// Fetch all namespaces using useList()
const [namespaces, namespacesError] = K8s.ResourceClasses.Namespace.useList();

if (!namespaces && !namespacesError) {
return (
<SectionBox title="Plugin Overview">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<CircularProgress size={20} />
<Typography>Loading namespaces...</Typography>
</Box>
</SectionBox>
);
}

if (namespacesError) {
return (
<SectionBox title="Plugin Overview">
<Typography color="error">
Error loading namespaces: {namespacesError.message}
</Typography>
</SectionBox>
);
}

return (
<SectionBox title="Plugin Overview">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<Typography>Current cluster:</Typography>
{cluster ? <>{cluster}</> : <>{'N/A'}</>}
</Box>

<Typography variant="h6" sx={{ mb: 1 }}>
Namespaces ({namespaces?.length || 0})
</Typography>

{/* Namespace buttons - click to see details */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 2 }}>
{namespaces?.slice(0, 5).map(ns => (
<Button
key={ns.metadata.name}
variant={selectedNamespace === ns.metadata.name ? 'contained' : 'outlined'}
size="small"
onClick={() => setSelectedNamespace(ns.metadata.name)}
>
{ns.metadata.name}
</Button>
))}
</Box>

{/* Show details when a namespace is selected */}
{selectedNamespace && (
<NamespaceDetail namespace={selectedNamespace} />
)}
</SectionBox>
);
}

registerRoute({
path: '/my-plugin',
sidebar: 'my-plugin-overview',
component: OverviewPage,
exact: true,
});

registerSidebarEntry({
name: 'my-plugin',
label: 'My Plugin',
icon: 'mdi:new-box',
url: '/my-plugin',
});

registerSidebarEntry({
parent: 'my-plugin',
name: 'my-plugin-overview',
label: 'Overview',
url: '/my-plugin',
});

Screenshot of the Overview page showing namespace buttons with a red rectangle highlighting the selected namespace details

What changed from the previous example:

AdditionPurpose
useState for selectedNamespaceTrack which namespace is selected
NamespaceDetail componentUses useGet() to fetch single namespace
Button instead of ListClickable namespace selection
Conditional renderOnly show details when a namespace is selected

useGet Signature:

Resource TypeSignatureExample
Cluster-scopeduseGet(name)Node.useGet('node-1')
NamespaceduseGet(name, namespace)Pod.useGet('nginx', 'default')

Direct API Calls with ApiProxy

Sometimes you need to call Kubernetes APIs that don't have a built-in resource class, or you want more control over the request. ApiProxy provides direct access to the Kubernetes API server.

Example: Add API Server Version to Overview Page

Let's enhance our OverviewPage to also display the Kubernetes API server version using ApiProxy.request(). This builds on the previous example:

import { useState, useEffect } from 'react';
import {
ApiProxy,
K8s,
registerRoute,
registerSidebarEntry,
} from '@kinvolk/headlamp-plugin/lib';
import { getCluster } from '@kinvolk/headlamp-plugin/lib/Utils';
import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import {
Typography,
Box,
CircularProgress,
Button,
Paper,
} from '@mui/material';

// Component to show details of a selected namespace
function NamespaceDetail({ namespace }: { namespace: string }) {
const [ns, nsError] = K8s.ResourceClasses.Namespace.useGet(namespace);

if (!ns && !nsError) {
return <CircularProgress size={20} />;
}

if (nsError) {
return <Typography color="error">Namespace not found</Typography>;
}

return (
<Paper variant="outlined" sx={{ p: 2 }}>
<Typography variant="h6">{ns.metadata.name}</Typography>
<Typography color="text.secondary">
Status: {ns.status?.phase}
</Typography>
<Typography color="text.secondary">
Created: {new Date(ns.metadata.creationTimestamp).toLocaleString()}
</Typography>
<Typography color="text.secondary">
Labels: {Object.keys(ns.metadata.labels || {}).length}
</Typography>
</Paper>
);
}

function OverviewPage() {
const cluster = getCluster();
const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null);
const [versionInfo, setVersionInfo] = useState<{ gitVersion?: string } | null>(null);

// Fetch all namespaces using useList()
const [namespaces, namespacesError] = K8s.ResourceClasses.Namespace.useList();

// Fetch API server version using ApiProxy
useEffect(() => {
async function fetchVersion() {
if (!cluster) return;

try {
const response = await ApiProxy.request('/version', {
cluster: cluster,
});
setVersionInfo(response);
} catch (err) {
// Silently fail - version info is optional
console.error('Failed to fetch version:', err);
}
}

fetchVersion();
}, [cluster]);

if (!namespaces && !namespacesError) {
return (
<SectionBox title="Plugin Overview">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<CircularProgress size={20} />
<Typography>Loading namespaces...</Typography>
</Box>
</SectionBox>
);
}

if (namespacesError) {
return (
<SectionBox title="Plugin Overview">
<Typography color="error">
Error loading namespaces: {namespacesError.message}
</Typography>
</SectionBox>
);
}

return (
<SectionBox title="Plugin Overview">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Typography>Current cluster:</Typography>
{cluster ? <>{cluster}</> : <>{'N/A'}</>}
</Box>

{/* API Server Version Info - added using ApiProxy */}
{versionInfo?.gitVersion && (
<Box sx={{ mb: 3 }}>
<Typography variant="body2" color="text.secondary">
API Server Version: {versionInfo.gitVersion}
</Typography>
</Box>
)}

<Typography variant="h6" sx={{ mb: 1 }}>
Namespaces ({namespaces?.length || 0})
</Typography>

{/* Namespace buttons - click to see details */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 2 }}>
{namespaces?.slice(0, 5).map(ns => (
<Button
key={ns.metadata.name}
variant={selectedNamespace === ns.metadata.name ? 'contained' : 'outlined'}
size="small"
onClick={() => setSelectedNamespace(ns.metadata.name)}
>
{ns.metadata.name}
</Button>
))}
</Box>

{/* Show details when a namespace is selected */}
{selectedNamespace && (
<NamespaceDetail namespace={selectedNamespace} />
)}
</SectionBox>
);
}

registerRoute({
path: '/my-plugin',
sidebar: 'my-plugin-overview',
component: OverviewPage,
exact: true,
});

registerSidebarEntry({
name: 'my-plugin',
label: 'My Plugin',
icon: 'mdi:new-box',
url: '/my-plugin',
});

registerSidebarEntry({
parent: 'my-plugin',
name: 'my-plugin-overview',
label: 'Overview',
url: '/my-plugin',
});

What's new:

  • Added ApiProxy import and useEffect hook
  • Added versionInfo state to store API server version
  • Added useEffect to fetch version when cluster changes
  • Display version info below cluster name

Screenshot of the Overview page showing cluster name, API server version highlighted with a red rectangle

Explore the Network Requests

To better understand how these API calls work, let's inspect the network traffic:

  1. Open your browser's Developer Tools (F12 or right-click → Inspect)
  2. Go to the Network tab
  3. Refresh your plugin page or navigate to it
  4. Look for the API requests:

You should see requests like:

  • /api/v1/namespaces - This is the useList() call fetching all namespaces
  • /version - This is the ApiProxy.request() call fetching API server version
  • /api/v1/namespaces/{name} - This appears when you click a namespace (the useGet() call)

Screenshot of the browser Developer Tools Network tab showing API requests highlighted by red lines

Understanding ApiProxy.request

const response = await ApiProxy.request('/version', {
cluster: cluster,
});
ParameterDescription
First arg: /versionThe API endpoint path (relative to cluster API server)
cluster optionWhich cluster to send the request to

Recommendations:

  • Use K8s.ResourceClasses for standard Kubernetes resources—it handles caching, error handling, and real-time updates automatically
  • Use ApiProxy.request() for custom endpoints or when you need full control
  • Always handle loading and error states for a good user experience

Troubleshooting

"No cluster selected" Error

Problem: getCluster() returns null

Solution: Make sure you're on a cluster-specific page:

  • URL should be /c/:cluster/your-path
  • Route should have useClusterURL: true (default)

Data Not Loading

Check the console for errors:

  1. Open browser DevTools (F12)
  2. Look at the Console tab for error messages
  3. Check the Network tab for failed API requests

Common issues:

  • RBAC permissions — User may not have permission to list resources
  • Cluster connectivity — Check if Headlamp can reach the cluster

useList Returns Empty Array

Possible causes:

  • The resource type doesn't exist in the cluster
  • No resources of that type exist
  • Filtered by namespace (for namespaced resources)

For namespaced resources, you can filter by namespace:

// List pods only in the 'default' namespace
const [pods, error] = K8s.ResourceClasses.Pod.useList({
namespace: 'default'
});

ApiProxy.request Fails

Check the endpoint path:

  • Ensure the path starts with /
  • Use the correct API group (/api/v1/ vs /apis/apps/v1/)

Check cluster parameter:

const cluster = getCluster();
if (!cluster) {
// Handle no cluster case
return;
}

const response = await ApiProxy.request('/version', {
cluster: cluster, // Must be provided
});

What's Next

You've learned three ways to fetch Kubernetes data:

  • getCluster() for cluster context
  • K8s.ResourceClasses.useList() for listing resources
  • K8s.ResourceClasses.useGet() for single resources
  • ApiProxy.request() for direct API calls
  • ✅ Handling loading and error states

In the next tutorial, you'll learn advanced techniques:

Coming up in Tutorial 5: Advanced Kubernetes Interactions

  • Creating custom resource classes for type-safe data access
  • Using ApiProxy to modify resources (add labels, update resources)
  • Error handling for write operations

Quick Reference

getCluster

import { getCluster } from '@kinvolk/headlamp-plugin/lib/Utils';

const cluster = getCluster(); // Returns cluster name or null

K8s.ResourceClasses

import { K8s } from '@kinvolk/headlamp-plugin/lib';

// List all resources
const [items, error] = K8s.ResourceClasses.Namespace.useList();

// List with filters
const [pods, error] = K8s.ResourceClasses.Pod.useList({
namespace: 'default',
});

// Get single resource (cluster-scoped)
const [node, error] = K8s.ResourceClasses.Node.useGet('node-name');

// Get single resource (namespaced)
const [pod, error] = K8s.ResourceClasses.Pod.useGet('pod-name', 'namespace');

ApiProxy.request

import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';

// GET request
const response = await ApiProxy.request('/api/v1/namespaces', {
cluster: 'my-cluster',
});

// With error handling
try {
const data = await ApiProxy.request('/version', { cluster });
} catch (error) {
console.error('Request failed:', error);
}