# Directory Structure
```
├── .github
│ └── workflows
│ └── build.yml
├── .gitignore
├── cmd
│ └── harvester-mcp-server
│ └── main.go
├── Dockerfile
├── go.mod
├── go.sum
├── LICENSE
├── Makefile
├── pkg
│ ├── client
│ │ └── client.go
│ ├── cmd
│ │ └── root.go
│ ├── kubernetes
│ │ ├── core_formatters.go
│ │ ├── formatters.go
│ │ ├── harvester_formatters.go
│ │ ├── resources.go
│ │ └── types.go
│ └── mcp
│ └── server.go
└── README.md
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
# IDE files
.idea/
.vscode/
*.swp
*.swo
# OS specific files
.DS_Store
Thumbs.db
# Build artifacts
bin/
harvester-mcp-server
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Harvester MCP Server
Model Context Protocol (MCP) server for Harvester HCI that enables Claude Desktop, Cursor, and other AI assistants to interact with Harvester clusters through the MCP protocol.
## Overview
Harvester MCP Server is a Go implementation of the [Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io/specification/2024-11-05/) specifically designed for [Harvester HCI](https://github.com/harvester/harvester). It allows AI assistants like Claude Desktop and Cursor to perform CRUD operations on Harvester clusters, which are essentially Kubernetes clusters with Harvester-specific CRDs.
## Workflow
The following diagram illustrates how Harvester MCP Server bridges the gap between AI assistants and Harvester clusters:
```mermaid
graph LR;
subgraph "AI Assistants"
A[Claude Desktop] --> C[MCP Client];
B[Cursor IDE] --> C;
end
subgraph "Harvester MCP Server"
C --> D[MCP Server];
D --> E[Resource Handler];
E --> F[Formatter Registry];
F -->|Get Formatter| G[Core Resource Formatters];
F -->|Get Formatter| H[Harvester Resource Formatters];
end
subgraph "Kubernetes / Harvester"
G --> I[Kubernetes API];
H --> I;
I --> J[Harvester Cluster];
end
style A fill:#f9f,stroke:#333,stroke-width:2px;
style B fill:#f9f,stroke:#333,stroke-width:2px;
style D fill:#bbf,stroke:#333,stroke-width:2px;
style J fill:#bfb,stroke:#333,stroke-width:2px;
```
### How It Works
1. **LLM Integration**: AI assistants like Claude Desktop and Cursor connect to Harvester MCP Server via the MCP protocol.
2. **Request Processing**: The MCP Server receives natural language requests from the AI assistants and translates them into specific Kubernetes operations.
3. **Resource Handling**: The Resource Handler identifies the resource type and operation being requested.
4. **Formatter Selection**: The Formatter Registry selects the appropriate formatter for the resource type.
5. **API Interaction**: The server interacts with the Kubernetes API of the Harvester cluster.
6. **Response Formatting**: Results are formatted into human-readable text optimized for LLM consumption.
7. **User Presentation**: Formatted responses are returned to the AI assistant to present to the user.
This architecture enables AI assistants to interact with Harvester clusters through natural language, making complex Kubernetes operations more accessible to users.
## Features
- **Kubernetes Core Resources**:
- Pods: List, Get, Delete
- Deployments: List, Get
- Services: List, Get
- Namespaces: List, Get
- Nodes: List, Get
- Custom Resource Definitions (CRDs): List
- **Harvester-Specific Resources**:
- Virtual Machines: List, Get
- Images: List
- Volumes: List
- Networks: List
- **Enhanced User Experience**:
- Human-readable formatted outputs for all resources
- Automatic grouping of resources by namespace or status
- Concise summaries with the most relevant information
- Detailed views for comprehensive resource inspection
## Requirements
- Go 1.23+
- Access to a Harvester cluster with a valid kubeconfig
## Installation
### From Source
```bash
# Clone the repository
git clone https://github.com/starbops/harvester-mcp-server.git
cd harvester-mcp-server
# Build
make build
# Run
./bin/harvester-mcp-server
```
### Using Go Install
```bash
go install github.com/starbops/harvester-mcp-server/cmd/harvester-mcp-server@latest
```
## Configuration
The server automatically looks for Kubernetes configuration in the following order:
1. In-cluster configuration (if running inside a Kubernetes cluster)
2. Path specified by the `--kubeconfig` flag
3. Path specified by the `KUBECONFIG` environment variable
4. Default location at `~/.kube/config`
### Command-Line Flags
```
Usage:
harvester-mcp-server [flags]
Flags:
-h, --help help for harvester-mcp-server
--kubeconfig string Path to the kubeconfig file (default is $KUBECONFIG or $HOME/.kube/config)
--log-level string Log level (debug, info, warn, error, fatal, panic) (default "info")
```
### Examples
Using a specific kubeconfig file:
```bash
harvester-mcp-server --kubeconfig=/path/to/kubeconfig.yaml
```
Using the KUBECONFIG environment variable:
```bash
export KUBECONFIG=$HOME/config.yaml
harvester-mcp-server
```
With debug logging:
```bash
harvester-mcp-server --log-level=debug
```
## Usage with Claude Desktop
1. Install Claude Desktop
2. Open Claude Desktop configuration file (`~/Library/Application\ Support/Claude/claude_desktop_config.json` or similar)
3. Add the Harvester MCP server to the `mcpServers` section:
```json
{
"mcpServers": {
"harvester": {
"command": "/path/to/harvester-mcp-server",
"args": ["--kubeconfig", "/path/to/kubeconfig.yaml", "--log-level", "info"]
}
}
}
```
4. Restart Claude Desktop
5. The Harvester MCP tools should now be available to Claude
## Example Queries for Claude Desktop
Once your Harvester MCP server is configured in Claude Desktop, you can ask questions like:
- "How many nodes are in my Harvester cluster?"
- "List all pods in the cattle-system namespace"
- "Show me the details of the pod named rancher-789c976c6-xbvmd in cattle-system namespace"
- "List all virtual machines in the default namespace"
- "What services are running in the harvester-system namespace?"
## Development
### Project Structure
- `cmd/harvester-mcp-server`: Main application entry point
- `pkg/client`: Kubernetes client implementation
- `pkg/cmd`: CLI commands implementation using Cobra
- `pkg/mcp`: MCP server implementation
- `pkg/kubernetes`: Unified resource handlers for Kubernetes resources
- `pkg/tools`: Legacy tool implementations for interacting with Harvester resources
### Resource Handling
The project uses a unified approach to handle Kubernetes resources:
1. The `pkg/kubernetes/resources.go` file contains a `ResourceHandler` that provides common operations for all resource types:
- Listing resources with proper namespace handling
- Getting resource details by name
- Creating and updating resources
- Deleting resources
2. The `pkg/kubernetes/formatters*.go` files contain formatters for different resource types:
- Each formatter converts raw Kubernetes objects into human-readable text
- Resource-specific details are extracted and formatted in a consistent way
- Custom formatters exist for both standard Kubernetes resources and Harvester-specific resources
3. The `pkg/kubernetes/types.go` file maps friendly resource type names to Kubernetes GroupVersionResource objects:
- Makes it easy to refer to resources by simple names in code
- Centralizes resource type definitions
### Adding New Tools
To add a new tool:
1. If it's a new resource type, add it to `pkg/kubernetes/types.go`
2. Implement formatters for the resource in one of the formatter files
3. Register the tool in `pkg/mcp/server.go` in the `registerTools` method using the unified resource handler
### Formatting Functions
Each resource type has two formatting functions:
1. `formatXList` - Formats a list of resources with concise information, grouped by namespace
2. `formatX` - Formats a single resource with detailed information
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
## Acknowledgments
- [Harvester HCI](https://github.com/harvester/harvester) - The foundation for this project
- [mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) - The Go SDK for Model Context Protocol
- [manusa/kubernetes-mcp-server](https://github.com/manusa/kubernetes-mcp-server) - Reference implementation for Kubernetes MCP server
```
--------------------------------------------------------------------------------
/cmd/harvester-mcp-server/main.go:
--------------------------------------------------------------------------------
```go
package main
import (
"github.com/starbops/harvester-mcp-server/pkg/cmd"
)
func main() {
cmd.Execute()
}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM docker.io/library/golang:1.23-alpine AS builder
WORKDIR /workspace
# Copy go.mod and go.sum
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy the source code
COPY . .
# Build the application
RUN go build -o harvester-mcp-server ./cmd/harvester-mcp-server
# Create a minimal runtime image
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
# Copy the binary from the builder stage
COPY --from=builder /workspace/harvester-mcp-server /app/harvester-mcp-server
# Set the entry point
ENTRYPOINT ["/app/harvester-mcp-server"]
```
--------------------------------------------------------------------------------
/pkg/cmd/root.go:
--------------------------------------------------------------------------------
```go
package cmd
import (
"fmt"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/starbops/harvester-mcp-server/pkg/mcp"
)
var (
// Global flags
kubeConfigPath string
logLevel string
// Root command
rootCmd = &cobra.Command{
Use: "harvester-mcp-server",
Short: "Harvester MCP Server - MCP server for Harvester HCI",
Long: `Harvester MCP Server is a Model Context Protocol (MCP) server for Harvester HCI.
It allows AI assistants like Claude and Cursor to interact with Harvester clusters through the MCP protocol.`,
RunE: func(cmd *cobra.Command, args []string) error {
// Configure log level
level, err := log.ParseLevel(logLevel)
if err != nil {
log.Warnf("Invalid log level '%s', defaulting to 'info'", logLevel)
level = log.InfoLevel
}
log.SetLevel(level)
return runServer()
},
// Disable the automatic help message when an error occurs
SilenceUsage: true,
// Disable automatic error printing since we'll handle it explicitly
SilenceErrors: true,
}
)
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Fatalf("%v", err)
}
}
func init() {
// Configure default logger settings
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
// Add flags
rootCmd.PersistentFlags().StringVar(&kubeConfigPath, "kubeconfig", "", "Path to the kubeconfig file (default is $KUBECONFIG or $HOME/.kube/config)")
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Log level (debug, info, warn, error, fatal, panic)")
}
func runServer() error {
log.Info("Starting Harvester MCP Server")
// Create server configuration
cfg := &mcp.Config{
KubeConfigPath: kubeConfigPath,
}
// Create and start the MCP server
log.Debug("Creating MCP server instance")
server, err := mcp.NewServer(cfg)
if err != nil {
return fmt.Errorf("failed to create MCP server: %w", err)
}
// Start the server
log.Info("Starting MCP server (using stdio for communication)")
if err := server.ServeStdio(); err != nil {
return fmt.Errorf("failed to start MCP server: %w", err)
}
return nil
}
```
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
```yaml
name: Build and Release
on:
push:
branches: [ main ]
# Enable manual trigger for testing purposes
workflow_dispatch:
jobs:
build:
name: Build and Upload
runs-on: ubuntu-latest
permissions:
contents: write # Required for uploading to GitHub releases
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
check-latest: true
cache: true
- name: Build for Linux (amd64)
run: |
mkdir -p bin
GOOS=linux GOARCH=amd64 go build -o bin/harvester-mcp-server-linux-amd64 ./cmd/harvester-mcp-server
chmod +x bin/harvester-mcp-server-linux-amd64
- name: Build for Linux (arm64)
run: |
GOOS=linux GOARCH=arm64 go build -o bin/harvester-mcp-server-linux-arm64 ./cmd/harvester-mcp-server
chmod +x bin/harvester-mcp-server-linux-arm64
- name: Build for macOS (amd64)
run: |
GOOS=darwin GOARCH=amd64 go build -o bin/harvester-mcp-server-darwin-amd64 ./cmd/harvester-mcp-server
chmod +x bin/harvester-mcp-server-darwin-amd64
- name: Build for macOS (arm64)
run: |
GOOS=darwin GOARCH=arm64 go build -o bin/harvester-mcp-server-darwin-arm64 ./cmd/harvester-mcp-server
chmod +x bin/harvester-mcp-server-darwin-arm64
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: harvester-mcp-server-binaries
path: bin/
retention-days: 7
- name: Create release tag
id: tag
run: |
TIMESTAMP=$(date +'%Y%m%d%H%M%S')
echo "tag=release-${TIMESTAMP}" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
name: Automatic Build ${{ steps.tag.outputs.tag }}
tag_name: ${{ steps.tag.outputs.tag }}
files: |
bin/harvester-mcp-server-linux-amd64
bin/harvester-mcp-server-linux-arm64
bin/harvester-mcp-server-darwin-amd64
bin/harvester-mcp-server-darwin-arm64
generate_release_notes: true
prerelease: true
fail_on_unmatched_files: true
```
--------------------------------------------------------------------------------
/pkg/client/client.go:
--------------------------------------------------------------------------------
```go
package client
import (
"fmt"
"os"
"path/filepath"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
// Config represents the configuration for the Kubernetes client.
type Config struct {
// KubeConfigPath is the path to the kubeconfig file.
// If empty, it defaults to the KUBECONFIG environment variable,
// then to ~/.kube/config.
KubeConfigPath string
}
// Client represents a Kubernetes client for interacting with Harvester clusters.
type Client struct {
Clientset *kubernetes.Clientset
Config *rest.Config
}
// NewClient creates a new Kubernetes client.
func NewClient(cfg *Config) (*Client, error) {
config, err := getKubeConfig(cfg.KubeConfigPath)
if err != nil {
return nil, fmt.Errorf("failed to get Kubernetes config: %w", err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to create Kubernetes clientset: %w", err)
}
return &Client{
Clientset: clientset,
Config: config,
}, nil
}
// getKubeConfig returns a Kubernetes configuration.
func getKubeConfig(kubeConfigPath string) (*rest.Config, error) {
// Try to use in-cluster config if running in a Kubernetes cluster
config, err := rest.InClusterConfig()
if err == nil {
return config, nil
}
// If kubeConfigPath is specified, use it
if kubeConfigPath != "" {
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
if err != nil {
return nil, fmt.Errorf("failed to build config from specified kubeconfig file %s: %w", kubeConfigPath, err)
}
return config, nil
}
// Check KUBECONFIG environment variable
envKubeconfig := os.Getenv("KUBECONFIG")
if envKubeconfig != "" {
config, err := clientcmd.BuildConfigFromFlags("", envKubeconfig)
if err != nil {
return nil, fmt.Errorf("failed to build config from KUBECONFIG environment variable %s: %w", envKubeconfig, err)
}
return config, nil
}
// Fall back to default kubeconfig location
kubeconfig := filepath.Join(homeDir(), ".kube", "config")
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, fmt.Errorf("failed to build config from default kubeconfig file: %w", err)
}
return config, nil
}
// homeDir returns the user's home directory.
func homeDir() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return home
}
```
--------------------------------------------------------------------------------
/pkg/kubernetes/types.go:
--------------------------------------------------------------------------------
```go
package kubernetes
import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
// Define constants for supported resource types
const (
ResourceTypePod = "pod"
ResourceTypePods = "pods"
ResourceTypeDeployment = "deployment"
ResourceTypeDeployments = "deployments"
ResourceTypeService = "service"
ResourceTypeServices = "services"
ResourceTypeNamespace = "namespace"
ResourceTypeNamespaces = "namespaces"
ResourceTypeNode = "node"
ResourceTypeNodes = "nodes"
ResourceTypeCRD = "crd"
ResourceTypeCRDs = "crds"
ResourceTypeVM = "vm"
ResourceTypeVMs = "vms"
ResourceTypeVolume = "volume"
ResourceTypeVolumes = "volumes"
ResourceTypeNetwork = "network"
ResourceTypeNetworks = "networks"
ResourceTypeImage = "image"
ResourceTypeImages = "images"
)
// ResourceTypeToGVR maps friendly resource type names to GroupVersionResource
var ResourceTypeToGVR = map[string]schema.GroupVersionResource{
// Core Kubernetes resources
ResourceTypePod: {Group: "", Version: "v1", Resource: "pods"},
ResourceTypePods: {Group: "", Version: "v1", Resource: "pods"},
ResourceTypeService: {Group: "", Version: "v1", Resource: "services"},
ResourceTypeServices: {Group: "", Version: "v1", Resource: "services"},
ResourceTypeNamespace: {Group: "", Version: "v1", Resource: "namespaces"},
ResourceTypeNamespaces: {Group: "", Version: "v1", Resource: "namespaces"},
ResourceTypeNode: {Group: "", Version: "v1", Resource: "nodes"},
ResourceTypeNodes: {Group: "", Version: "v1", Resource: "nodes"},
ResourceTypeDeployment: {Group: "apps", Version: "v1", Resource: "deployments"},
ResourceTypeDeployments: {Group: "apps", Version: "v1", Resource: "deployments"},
ResourceTypeCRD: {Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"},
ResourceTypeCRDs: {Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"},
// Harvester-specific resources
ResourceTypeVM: {Group: "kubevirt.io", Version: "v1", Resource: "virtualmachines"},
ResourceTypeVMs: {Group: "kubevirt.io", Version: "v1", Resource: "virtualmachines"},
ResourceTypeVolume: {Group: "storage.harvesterhci.io", Version: "v1beta1", Resource: "volumes"},
ResourceTypeVolumes: {Group: "storage.harvesterhci.io", Version: "v1beta1", Resource: "volumes"},
ResourceTypeNetwork: {Group: "network.harvesterhci.io", Version: "v1beta1", Resource: "networks"},
ResourceTypeNetworks: {Group: "network.harvesterhci.io", Version: "v1beta1", Resource: "networks"},
ResourceTypeImage: {Group: "harvesterhci.io", Version: "v1beta1", Resource: "virtualmachineimages"},
ResourceTypeImages: {Group: "harvesterhci.io", Version: "v1beta1", Resource: "virtualmachineimages"},
}
// GVRToResourceType maps GroupVersionResource to friendly resource type names
var GVRToResourceType = map[schema.GroupVersionResource]string{
// Core Kubernetes resources
{Group: "", Version: "v1", Resource: "pods"}: ResourceTypePod,
{Group: "", Version: "v1", Resource: "services"}: ResourceTypeService,
{Group: "", Version: "v1", Resource: "namespaces"}: ResourceTypeNamespace,
{Group: "", Version: "v1", Resource: "nodes"}: ResourceTypeNode,
{Group: "apps", Version: "v1", Resource: "deployments"}: ResourceTypeDeployment,
{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}: ResourceTypeCRD,
// Harvester-specific resources
{Group: "kubevirt.io", Version: "v1", Resource: "virtualmachines"}: ResourceTypeVM,
{Group: "storage.harvesterhci.io", Version: "v1beta1", Resource: "volumes"}: ResourceTypeVolume,
{Group: "network.harvesterhci.io", Version: "v1beta1", Resource: "networks"}: ResourceTypeNetwork,
{Group: "harvesterhci.io", Version: "v1beta1", Resource: "virtualmachineimages"}: ResourceTypeImage,
}
```
--------------------------------------------------------------------------------
/pkg/kubernetes/resources.go:
--------------------------------------------------------------------------------
```go
package kubernetes
import (
"context"
"fmt"
"strings"
"time"
"github.com/starbops/harvester-mcp-server/pkg/client"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/restmapper"
)
// ResourceHandler provides a unified interface for handling Kubernetes resources.
type ResourceHandler struct {
client *client.Client
dynamicClient dynamic.Interface
k8sClient *kubernetes.Clientset
mapper *restmapper.DeferredDiscoveryRESTMapper
}
// NewResourceHandler creates a new ResourceHandler instance.
func NewResourceHandler(client *client.Client) (*ResourceHandler, error) {
dynamicClient, err := dynamic.NewForConfig(client.Config)
if err != nil {
return nil, fmt.Errorf("failed to create dynamic client: %w", err)
}
return &ResourceHandler{
client: client,
dynamicClient: dynamicClient,
k8sClient: client.Clientset,
}, nil
}
// ListResources retrieves a list of resources of the specified type.
func (h *ResourceHandler) ListResources(ctx context.Context, gvr schema.GroupVersionResource, namespace string) (*unstructured.UnstructuredList, error) {
if namespace == "" {
return h.dynamicClient.Resource(gvr).List(ctx, metav1.ListOptions{})
}
return h.dynamicClient.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
}
// GetResource retrieves a specific resource by name.
func (h *ResourceHandler) GetResource(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) {
return h.dynamicClient.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
}
// CreateResource creates a new resource.
func (h *ResourceHandler) CreateResource(ctx context.Context, gvr schema.GroupVersionResource, namespace string, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return h.dynamicClient.Resource(gvr).Namespace(namespace).Create(ctx, obj, metav1.CreateOptions{})
}
// UpdateResource updates an existing resource.
func (h *ResourceHandler) UpdateResource(ctx context.Context, gvr schema.GroupVersionResource, namespace string, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return h.dynamicClient.Resource(gvr).Namespace(namespace).Update(ctx, obj, metav1.UpdateOptions{})
}
// DeleteResource deletes a resource by name.
func (h *ResourceHandler) DeleteResource(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) error {
return h.dynamicClient.Resource(gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}
// IsNamespaced determines if a resource type is namespaced or cluster-scoped.
func (h *ResourceHandler) IsNamespaced(gvr schema.GroupVersionResource) (bool, error) {
apiResourceList, err := h.k8sClient.Discovery().ServerResourcesForGroupVersion(gvr.GroupVersion().String())
if err != nil {
return false, err
}
for _, apiResource := range apiResourceList.APIResources {
if apiResource.Name == gvr.Resource {
return apiResource.Namespaced, nil
}
}
return false, fmt.Errorf("resource %s not found in group version %s", gvr.Resource, gvr.GroupVersion().String())
}
// FormatResourceList formats a list of resources into a human-readable string based on resource type.
func (h *ResourceHandler) FormatResourceList(list *unstructured.UnstructuredList, gvr schema.GroupVersionResource) string {
switch {
case gvr.Resource == "pods" && gvr.Group == "":
return formatPodList(list)
case gvr.Resource == "services" && gvr.Group == "":
return formatServiceList(list)
case gvr.Resource == "namespaces" && gvr.Group == "":
return formatNamespaceList(list)
case gvr.Resource == "nodes" && gvr.Group == "":
return formatNodeList(list)
case gvr.Resource == "deployments" && gvr.Group == "apps":
return formatDeploymentList(list)
case gvr.Resource == "virtualmachines" && gvr.Group == "kubevirt.io":
return formatVirtualMachineList(list)
case gvr.Resource == "networks" && gvr.Group == "network.harvesterhci.io":
return formatNetworkList(list)
case gvr.Resource == "volumes" && gvr.Group == "storage.harvesterhci.io":
return formatVolumeList(list)
case gvr.Resource == "virtualmachineimages" && gvr.Group == "harvesterhci.io":
return formatImageList(list)
case gvr.Resource == "customresourcedefinitions" && gvr.Group == "apiextensions.k8s.io":
return formatCRDList(list)
default:
// Generic formatter for unsupported resource types
return formatGenericResourceList(list, gvr)
}
}
// FormatResource formats a single resource into a human-readable string based on resource type.
func (h *ResourceHandler) FormatResource(resource *unstructured.Unstructured, gvr schema.GroupVersionResource) string {
switch {
case gvr.Resource == "pods" && gvr.Group == "":
return formatPod(resource)
case gvr.Resource == "services" && gvr.Group == "":
return formatService(resource)
case gvr.Resource == "namespaces" && gvr.Group == "":
return formatNamespace(resource)
case gvr.Resource == "nodes" && gvr.Group == "":
return formatNode(resource)
case gvr.Resource == "deployments" && gvr.Group == "apps":
return formatDeployment(resource)
case gvr.Resource == "virtualmachines" && gvr.Group == "kubevirt.io":
return formatVirtualMachine(resource)
case gvr.Resource == "networks" && gvr.Group == "network.harvesterhci.io":
return formatNetwork(resource)
case gvr.Resource == "volumes" && gvr.Group == "storage.harvesterhci.io":
return formatVolume(resource)
case gvr.Resource == "virtualmachineimages" && gvr.Group == "harvesterhci.io":
return formatImage(resource)
case gvr.Resource == "customresourcedefinitions" && gvr.Group == "apiextensions.k8s.io":
return formatCRD(resource)
default:
// Generic formatter for unsupported resource types
return formatGenericResource(resource, gvr)
}
}
// formatGenericResourceList creates a generic human-readable representation of resources
func formatGenericResourceList(list *unstructured.UnstructuredList, gvr schema.GroupVersionResource) string {
if len(list.Items) == 0 {
return fmt.Sprintf("No %s found in the specified namespace(s).", gvr.Resource)
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d %s:\n\n", len(list.Items), gvr.Resource))
// Group resources by namespace if they are namespaced
resourcesByNamespace := make(map[string][]unstructured.Unstructured)
for _, item := range list.Items {
namespace := item.GetNamespace()
if namespace == "" {
namespace = "cluster-scoped"
}
resourcesByNamespace[namespace] = append(resourcesByNamespace[namespace], item)
}
// Print resources grouped by namespace
for namespace, items := range resourcesByNamespace {
if namespace == "cluster-scoped" {
sb.WriteString(fmt.Sprintf("Cluster-scoped resources (%d items)\n", len(items)))
} else {
sb.WriteString(fmt.Sprintf("Namespace: %s (%d items)\n", namespace, len(items)))
}
for _, item := range items {
sb.WriteString(fmt.Sprintf(" • %s\n", item.GetName()))
// Add creation time
creationTime := item.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
// Add basic info from status if available
status, found, _ := unstructured.NestedMap(item.Object, "status")
if found {
sb.WriteString(" Status:\n")
for key, value := range status {
sb.WriteString(fmt.Sprintf(" %s: %v\n", key, value))
}
}
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
// formatGenericResource creates a generic human-readable representation of a single resource
func formatGenericResource(resource *unstructured.Unstructured, gvr schema.GroupVersionResource) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("%s: %s\n", strings.Title(gvr.Resource), resource.GetName()))
if namespace := resource.GetNamespace(); namespace != "" {
sb.WriteString(fmt.Sprintf("Namespace: %s\n", namespace))
} else {
sb.WriteString("Scope: Cluster-wide\n")
}
// Creation time
creationTime := resource.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("Created: %s\n", creationTime))
// Labels
if labels := resource.GetLabels(); len(labels) > 0 {
sb.WriteString("\nLabels:\n")
for key, value := range labels {
sb.WriteString(fmt.Sprintf(" %s: %s\n", key, value))
}
}
// Annotations
if annotations := resource.GetAnnotations(); len(annotations) > 0 {
sb.WriteString("\nAnnotations:\n")
for key, value := range annotations {
sb.WriteString(fmt.Sprintf(" %s: %s\n", key, value))
}
}
// Spec
spec, found, _ := unstructured.NestedMap(resource.Object, "spec")
if found {
sb.WriteString("\nSpec:\n")
for key, value := range spec {
sb.WriteString(fmt.Sprintf(" %s: %v\n", key, value))
}
}
// Status
status, found, _ := unstructured.NestedMap(resource.Object, "status")
if found {
sb.WriteString("\nStatus:\n")
for key, value := range status {
sb.WriteString(fmt.Sprintf(" %s: %v\n", key, value))
}
}
return sb.String()
}
// Helper function to safely get a nested string from an unstructured object
func getNestedString(obj map[string]interface{}, fields ...string) string {
val, found, _ := unstructured.NestedString(obj, fields...)
if !found {
return ""
}
return val
}
// Helper function to safely get a nested int64 from an unstructured object
func getNestedInt64(obj map[string]interface{}, fields ...string) int64 {
val, found, _ := unstructured.NestedInt64(obj, fields...)
if !found {
return 0
}
return val
}
// Helper function to safely get a nested bool from an unstructured object
func getNestedBool(obj map[string]interface{}, fields ...string) bool {
val, found, _ := unstructured.NestedBool(obj, fields...)
if !found {
return false
}
return val
}
// Helper function to safely get a nested string slice from an unstructured object
func getNestedStringSlice(obj map[string]interface{}, fields ...string) []string {
val, found, _ := unstructured.NestedStringSlice(obj, fields...)
if !found {
return []string{}
}
return val
}
// Helper function to safely get a nested map from an unstructured object
func getNestedMap(obj map[string]interface{}, fields ...string) map[string]interface{} {
val, found, _ := unstructured.NestedMap(obj, fields...)
if !found {
return map[string]interface{}{}
}
return val
}
```
--------------------------------------------------------------------------------
/pkg/kubernetes/formatters.go:
--------------------------------------------------------------------------------
```go
package kubernetes
import (
"fmt"
"strings"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// ResourceFormatter defines the interface for formatting Kubernetes resources
type ResourceFormatter interface {
FormatResource(res *unstructured.Unstructured) string
FormatResourceList(list *unstructured.UnstructuredList) string
}
// FormatterRegistry maintains a mapping of resource kinds to their formatters
type FormatterRegistry struct {
formatters map[string]ResourceFormatter
}
// NewFormatterRegistry creates a new registry with all registered formatters
func NewFormatterRegistry() *FormatterRegistry {
registry := &FormatterRegistry{
formatters: make(map[string]ResourceFormatter),
}
// Register core Kubernetes formatters
registry.Register("Pod", &PodFormatter{})
registry.Register("Service", &ServiceFormatter{})
registry.Register("Namespace", &NamespaceFormatter{})
registry.Register("Node", &NodeFormatter{})
registry.Register("Deployment", &DeploymentFormatter{})
// Register Harvester specific formatters
registry.Register("VirtualMachine", &VirtualMachineFormatter{})
registry.Register("Volume", &VolumeFormatter{})
registry.Register("Network", &NetworkFormatter{})
registry.Register("VirtualMachineImage", &VMImageFormatter{})
registry.Register("CustomResourceDefinition", &CRDFormatter{})
return registry
}
// defaultRegistry is a package-level registry instance for use by backward compatibility functions
var defaultRegistry = NewFormatterRegistry()
// Register adds a new formatter to the registry
func (r *FormatterRegistry) Register(kind string, formatter ResourceFormatter) {
r.formatters[kind] = formatter
}
// GetFormatter returns the formatter for a specific resource kind
func (r *FormatterRegistry) GetFormatter(kind string) (ResourceFormatter, bool) {
formatter, exists := r.formatters[kind]
return formatter, exists
}
// FormatResource formats a single resource using the appropriate formatter
func (r *FormatterRegistry) FormatResource(res *unstructured.Unstructured) string {
kind := res.GetKind()
if formatter, exists := r.GetFormatter(kind); exists {
return formatter.FormatResource(res)
}
return genericResourceFormatter(res)
}
// FormatResourceList formats a list of resources using the appropriate formatter
func (r *FormatterRegistry) FormatResourceList(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No resources found in the specified namespace(s)."
}
// Determine the kind from the first item
if len(list.Items) > 0 {
kind := list.Items[0].GetKind()
if formatter, exists := r.GetFormatter(kind); exists {
return formatter.FormatResourceList(list)
}
}
return genericResourceListFormatter(list)
}
// genericResourceFormatter creates a human-readable representation of any resource
func genericResourceFormatter(res *unstructured.Unstructured) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Kind: %s\n", res.GetKind()))
sb.WriteString(fmt.Sprintf("Name: %s\n", res.GetName()))
if ns := res.GetNamespace(); ns != "" {
sb.WriteString(fmt.Sprintf("Namespace: %s\n", ns))
}
creationTime := res.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("Created: %s\n", creationTime))
// Print labels if any
if labels := res.GetLabels(); len(labels) > 0 {
sb.WriteString("\nLabels:\n")
for key, value := range labels {
sb.WriteString(fmt.Sprintf(" %s: %s\n", key, value))
}
}
return sb.String()
}
// genericResourceListFormatter creates a human-readable list of any resources
func genericResourceListFormatter(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No resources found in the specified namespace(s)."
}
var sb strings.Builder
kind := "resources"
if len(list.Items) > 0 {
kind = list.Items[0].GetKind() + "s"
}
sb.WriteString(fmt.Sprintf("Found %d %s:\n\n", len(list.Items), kind))
// Group resources by namespace
resourcesByNamespace := make(map[string][]unstructured.Unstructured)
for _, item := range list.Items {
namespace := item.GetNamespace()
resourcesByNamespace[namespace] = append(resourcesByNamespace[namespace], item)
}
// Print resources grouped by namespace
for namespace, resources := range resourcesByNamespace {
if namespace == "" {
sb.WriteString(fmt.Sprintf("Cluster-scoped (%d %s)\n", len(resources), kind))
} else {
sb.WriteString(fmt.Sprintf("Namespace: %s (%d %s)\n", namespace, len(resources), kind))
}
for _, resource := range resources {
sb.WriteString(fmt.Sprintf(" • %s\n", resource.GetName()))
// Creation time
creationTime := resource.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
// The following functions maintain backward compatibility with any existing code that
// may call them directly. They now use the registry pattern internally.
// FormatPodList formats a list of Pod resources in a human-readable form
func FormatPodList(list *unstructured.UnstructuredList) string {
formatter, _ := defaultRegistry.GetFormatter("Pod")
return formatter.FormatResourceList(list)
}
// FormatPod formats a Pod resource in a human-readable form
func FormatPod(res *unstructured.Unstructured) string {
formatter, _ := defaultRegistry.GetFormatter("Pod")
return formatter.FormatResource(res)
}
// FormatServiceList formats a list of Service resources in a human-readable form
func FormatServiceList(list *unstructured.UnstructuredList) string {
formatter, _ := defaultRegistry.GetFormatter("Service")
return formatter.FormatResourceList(list)
}
// FormatService formats a Service resource in a human-readable form
func FormatService(res *unstructured.Unstructured) string {
formatter, _ := defaultRegistry.GetFormatter("Service")
return formatter.FormatResource(res)
}
// FormatNamespaceList formats a list of Namespace resources in a human-readable form
func FormatNamespaceList(list *unstructured.UnstructuredList) string {
formatter, _ := defaultRegistry.GetFormatter("Namespace")
return formatter.FormatResourceList(list)
}
// FormatNamespace formats a Namespace resource in a human-readable form
func FormatNamespace(res *unstructured.Unstructured) string {
formatter, _ := defaultRegistry.GetFormatter("Namespace")
return formatter.FormatResource(res)
}
// FormatNodeList formats a list of Node resources in a human-readable form
func FormatNodeList(list *unstructured.UnstructuredList) string {
formatter, _ := defaultRegistry.GetFormatter("Node")
return formatter.FormatResourceList(list)
}
// FormatNode formats a Node resource in a human-readable form
func FormatNode(res *unstructured.Unstructured) string {
formatter, _ := defaultRegistry.GetFormatter("Node")
return formatter.FormatResource(res)
}
// FormatDeploymentList formats a list of Deployment resources in a human-readable form
func FormatDeploymentList(list *unstructured.UnstructuredList) string {
formatter, _ := defaultRegistry.GetFormatter("Deployment")
return formatter.FormatResourceList(list)
}
// FormatDeployment formats a Deployment resource in a human-readable form
func FormatDeployment(res *unstructured.Unstructured) string {
formatter, _ := defaultRegistry.GetFormatter("Deployment")
return formatter.FormatResource(res)
}
// FormatVirtualMachineList formats a list of VirtualMachine resources in a human-readable form
func FormatVirtualMachineList(list *unstructured.UnstructuredList) string {
formatter, _ := defaultRegistry.GetFormatter("VirtualMachine")
return formatter.FormatResourceList(list)
}
// FormatVirtualMachine formats a VirtualMachine resource in a human-readable form
func FormatVirtualMachine(res *unstructured.Unstructured) string {
formatter, _ := defaultRegistry.GetFormatter("VirtualMachine")
return formatter.FormatResource(res)
}
// FormatVolumeList formats a list of Volume resources in a human-readable form
func FormatVolumeList(list *unstructured.UnstructuredList) string {
formatter, _ := defaultRegistry.GetFormatter("Volume")
return formatter.FormatResourceList(list)
}
// FormatVolume formats a Volume resource in a human-readable form
func FormatVolume(res *unstructured.Unstructured) string {
formatter, _ := defaultRegistry.GetFormatter("Volume")
return formatter.FormatResource(res)
}
// FormatNetworkList formats a list of Network resources in a human-readable form
func FormatNetworkList(list *unstructured.UnstructuredList) string {
formatter, _ := defaultRegistry.GetFormatter("Network")
return formatter.FormatResourceList(list)
}
// FormatNetwork formats a Network resource in a human-readable form
func FormatNetwork(res *unstructured.Unstructured) string {
formatter, _ := defaultRegistry.GetFormatter("Network")
return formatter.FormatResource(res)
}
// FormatImageList formats a list of VirtualMachineImage resources in a human-readable form
func FormatImageList(list *unstructured.UnstructuredList) string {
formatter, _ := defaultRegistry.GetFormatter("VirtualMachineImage")
return formatter.FormatResourceList(list)
}
// FormatImage formats a VirtualMachineImage resource in a human-readable form
func FormatImage(res *unstructured.Unstructured) string {
formatter, _ := defaultRegistry.GetFormatter("VirtualMachineImage")
return formatter.FormatResource(res)
}
// FormatCRDList formats a list of CustomResourceDefinition resources in a human-readable form
func FormatCRDList(list *unstructured.UnstructuredList) string {
formatter, _ := defaultRegistry.GetFormatter("CustomResourceDefinition")
return formatter.FormatResourceList(list)
}
// FormatCRD formats a CustomResourceDefinition resource in a human-readable form
func FormatCRD(res *unstructured.Unstructured) string {
formatter, _ := defaultRegistry.GetFormatter("CustomResourceDefinition")
return formatter.FormatResource(res)
}
// For backward compatibility with any code that may be using these unexported functions
var (
formatPodList = FormatPodList
formatPod = FormatPod
formatServiceList = FormatServiceList
formatService = FormatService
formatNamespaceList = FormatNamespaceList
formatNamespace = FormatNamespace
formatNodeList = FormatNodeList
formatNode = FormatNode
formatDeploymentList = FormatDeploymentList
formatDeployment = FormatDeployment
formatVirtualMachineList = FormatVirtualMachineList
formatVirtualMachine = FormatVirtualMachine
formatVolumeList = FormatVolumeList
formatVolume = FormatVolume
formatNetworkList = FormatNetworkList
formatNetwork = FormatNetwork
formatImageList = FormatImageList
formatImage = FormatImage
formatCRDList = FormatCRDList
formatCRD = FormatCRD
)
```
--------------------------------------------------------------------------------
/pkg/kubernetes/harvester_formatters.go:
--------------------------------------------------------------------------------
```go
package kubernetes
import (
"fmt"
"strings"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// VirtualMachineFormatter handles formatting for VirtualMachine resources
type VirtualMachineFormatter struct{}
func (f *VirtualMachineFormatter) FormatResource(res *unstructured.Unstructured) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Virtual Machine: %s\n", res.GetName()))
sb.WriteString(fmt.Sprintf("Namespace: %s\n", res.GetNamespace()))
// Get status
status := "Unknown"
running := getNestedBool(res.Object, "status", "ready")
created := getNestedBool(res.Object, "status", "created")
if running {
status = "Running"
} else if created {
status = "Created"
}
sb.WriteString(fmt.Sprintf("Status: %s\n", status))
// Running and created fields
sb.WriteString(fmt.Sprintf("Ready: %t\n", running))
sb.WriteString(fmt.Sprintf("Created: %t\n", created))
// Detailed VM specification
sb.WriteString("\nSpecification:\n")
// CPU and Memory
cpuCores := getNestedInt64(res.Object, "spec", "template", "spec", "domain", "cpu", "cores")
memory := getNestedString(res.Object, "spec", "template", "spec", "domain", "resources", "requests", "memory")
if cpuCores > 0 {
sb.WriteString(fmt.Sprintf(" CPU Cores: %d\n", cpuCores))
}
if memory != "" {
sb.WriteString(fmt.Sprintf(" Memory: %s\n", memory))
}
// Volumes
volumes, _, _ := unstructured.NestedSlice(res.Object, "spec", "template", "spec", "volumes")
if len(volumes) > 0 {
sb.WriteString("\nVolumes:\n")
for _, volumeObj := range volumes {
volume, ok := volumeObj.(map[string]interface{})
if !ok {
continue
}
name, _, _ := unstructured.NestedString(volume, "name")
sb.WriteString(fmt.Sprintf(" %s:\n", name))
// Check different volume types
if pvc, exists, _ := unstructured.NestedMap(volume, "persistentVolumeClaim"); exists && pvc != nil {
claimName := getNestedString(volume, "persistentVolumeClaim", "claimName")
sb.WriteString(fmt.Sprintf(" Type: PersistentVolumeClaim\n"))
sb.WriteString(fmt.Sprintf(" Claim Name: %s\n", claimName))
} else if container, exists, _ := unstructured.NestedMap(volume, "containerDisk"); exists && container != nil {
image := getNestedString(volume, "containerDisk", "image")
sb.WriteString(fmt.Sprintf(" Type: ContainerDisk\n"))
sb.WriteString(fmt.Sprintf(" Image: %s\n", image))
} else if cloudInit, exists, _ := unstructured.NestedMap(volume, "cloudInitNoCloud"); exists && cloudInit != nil {
sb.WriteString(fmt.Sprintf(" Type: CloudInitNoCloud\n"))
userData, userDataExists, _ := unstructured.NestedString(cloudInit, "userData")
if userDataExists && userData != "" {
sb.WriteString(fmt.Sprintf(" Has User Data: true\n"))
}
networkData, networkDataExists, _ := unstructured.NestedString(cloudInit, "networkData")
if networkDataExists && networkData != "" {
sb.WriteString(fmt.Sprintf(" Has Network Data: true\n"))
}
} else {
sb.WriteString(fmt.Sprintf(" Type: Other\n"))
}
}
}
// Networks
networks, _, _ := unstructured.NestedSlice(res.Object, "spec", "template", "spec", "networks")
if len(networks) > 0 {
sb.WriteString("\nNetworks:\n")
for _, netObj := range networks {
network, ok := netObj.(map[string]interface{})
if !ok {
continue
}
name, _, _ := unstructured.NestedString(network, "name")
sb.WriteString(fmt.Sprintf(" %s:\n", name))
// Check different network types
if podNet, exists, _ := unstructured.NestedString(network, "pod"); exists && podNet != "" {
sb.WriteString(fmt.Sprintf(" Type: Pod Network\n"))
} else if multus, exists, _ := unstructured.NestedMap(network, "multus"); exists && multus != nil {
networkName := getNestedString(network, "multus", "networkName")
sb.WriteString(fmt.Sprintf(" Type: Multus\n"))
sb.WriteString(fmt.Sprintf(" Network Name: %s\n", networkName))
} else {
sb.WriteString(fmt.Sprintf(" Type: Other\n"))
}
}
}
// Creation time
creationTime := res.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("\nCreated: %s\n", creationTime))
return sb.String()
}
func (f *VirtualMachineFormatter) FormatResourceList(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No virtual machines found in the specified namespace(s)."
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d virtual machine(s):\n\n", len(list.Items)))
// Group VMs by namespace
vmsByNamespace := make(map[string][]unstructured.Unstructured)
for _, item := range list.Items {
namespace := item.GetNamespace()
vmsByNamespace[namespace] = append(vmsByNamespace[namespace], item)
}
// Print VMs grouped by namespace
for namespace, vms := range vmsByNamespace {
sb.WriteString(fmt.Sprintf("Namespace: %s (%d VMs)\n", namespace, len(vms)))
for _, vm := range vms {
// Get status
status := "Unknown"
running := getNestedBool(vm.Object, "status", "ready")
created := getNestedBool(vm.Object, "status", "created")
if running {
status = "Running"
} else if created {
status = "Created"
}
// Get spec details
cpuCores := getNestedInt64(vm.Object, "spec", "template", "spec", "domain", "cpu", "cores")
memory := getNestedString(vm.Object, "spec", "template", "spec", "domain", "resources", "requests", "memory")
// Basic VM info
sb.WriteString(fmt.Sprintf(" • %s\n", vm.GetName()))
sb.WriteString(fmt.Sprintf(" Status: %s\n", status))
if cpuCores > 0 {
sb.WriteString(fmt.Sprintf(" CPU Cores: %d\n", cpuCores))
}
if memory != "" {
sb.WriteString(fmt.Sprintf(" Memory: %s\n", memory))
}
// Volumes
volumes, _, _ := unstructured.NestedSlice(vm.Object, "spec", "template", "spec", "volumes")
if len(volumes) > 0 {
sb.WriteString(" Volumes:\n")
for _, volumeObj := range volumes {
volume, ok := volumeObj.(map[string]interface{})
if !ok {
continue
}
name, _, _ := unstructured.NestedString(volume, "name")
// Check different volume types
if pvc, exists, _ := unstructured.NestedMap(volume, "persistentVolumeClaim"); exists && pvc != nil {
claimName := getNestedString(volume, "persistentVolumeClaim", "claimName")
sb.WriteString(fmt.Sprintf(" %s: PVC %s\n", name, claimName))
} else if container, exists, _ := unstructured.NestedMap(volume, "containerDisk"); exists && container != nil {
image := getNestedString(volume, "containerDisk", "image")
sb.WriteString(fmt.Sprintf(" %s: ContainerDisk %s\n", name, image))
} else if cloudInit, exists, _ := unstructured.NestedMap(volume, "cloudInitNoCloud"); exists && cloudInit != nil {
sb.WriteString(fmt.Sprintf(" %s: CloudInit\n", name))
} else {
sb.WriteString(fmt.Sprintf(" %s\n", name))
}
}
}
// Networks
networks, _, _ := unstructured.NestedSlice(vm.Object, "spec", "template", "spec", "networks")
if len(networks) > 0 {
sb.WriteString(" Networks:\n")
for _, netObj := range networks {
network, ok := netObj.(map[string]interface{})
if !ok {
continue
}
name, _, _ := unstructured.NestedString(network, "name")
// Check different network types
if podNet, exists, _ := unstructured.NestedString(network, "pod"); exists && podNet != "" {
sb.WriteString(fmt.Sprintf(" %s: Pod Network\n", name))
} else if multus, exists, _ := unstructured.NestedMap(network, "multus"); exists && multus != nil {
networkName := getNestedString(network, "multus", "networkName")
sb.WriteString(fmt.Sprintf(" %s: Multus %s\n", name, networkName))
} else {
sb.WriteString(fmt.Sprintf(" %s\n", name))
}
}
}
// Creation time
creationTime := vm.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
// VolumeFormatter handles formatting for Volume resources
type VolumeFormatter struct{}
func (f *VolumeFormatter) FormatResource(res *unstructured.Unstructured) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Volume: %s\n", res.GetName()))
sb.WriteString(fmt.Sprintf("Namespace: %s\n", res.GetNamespace()))
// Get size and status
size := getNestedString(res.Object, "spec", "size")
status := getNestedString(res.Object, "status", "state")
if status != "" {
sb.WriteString(fmt.Sprintf("Status: %s\n", status))
}
if size != "" {
sb.WriteString(fmt.Sprintf("Size: %s\n", size))
}
// Storage Class
storageClass := getNestedString(res.Object, "spec", "storageClassName")
if storageClass != "" {
sb.WriteString(fmt.Sprintf("Storage Class: %s\n", storageClass))
}
// Additional details
accessModes, _, _ := unstructured.NestedStringSlice(res.Object, "spec", "accessModes")
if len(accessModes) > 0 {
sb.WriteString("\nAccess Modes:\n")
for _, mode := range accessModes {
sb.WriteString(fmt.Sprintf(" %s\n", mode))
}
}
// Creation time
creationTime := res.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("\nCreated: %s\n", creationTime))
return sb.String()
}
func (f *VolumeFormatter) FormatResourceList(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No volumes found in the specified namespace(s)."
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d volume(s):\n\n", len(list.Items)))
// Group volumes by namespace
volumesByNamespace := make(map[string][]unstructured.Unstructured)
for _, item := range list.Items {
namespace := item.GetNamespace()
volumesByNamespace[namespace] = append(volumesByNamespace[namespace], item)
}
// Print volumes grouped by namespace
for namespace, volumes := range volumesByNamespace {
sb.WriteString(fmt.Sprintf("Namespace: %s (%d volumes)\n", namespace, len(volumes)))
for _, volume := range volumes {
// Get size and status
size := getNestedString(volume.Object, "spec", "size")
status := getNestedString(volume.Object, "status", "state")
// Basic volume info
sb.WriteString(fmt.Sprintf(" • %s\n", volume.GetName()))
if status != "" {
sb.WriteString(fmt.Sprintf(" Status: %s\n", status))
}
if size != "" {
sb.WriteString(fmt.Sprintf(" Size: %s\n", size))
}
// Creation time
creationTime := volume.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
// NetworkFormatter handles formatting for Network resources
type NetworkFormatter struct{}
func (f *NetworkFormatter) FormatResource(res *unstructured.Unstructured) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Network: %s\n", res.GetName()))
sb.WriteString(fmt.Sprintf("Namespace: %s\n", res.GetNamespace()))
// Get network type and config
networkType := getNestedString(res.Object, "spec", "type")
if networkType != "" {
sb.WriteString(fmt.Sprintf("Type: %s\n", networkType))
}
// Config details
config, configFound, _ := unstructured.NestedMap(res.Object, "spec", "config")
if configFound && len(config) > 0 {
sb.WriteString("\nConfiguration:\n")
for key, value := range config {
sb.WriteString(fmt.Sprintf(" %s: %v\n", key, value))
}
}
// Creation time
creationTime := res.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("\nCreated: %s\n", creationTime))
return sb.String()
}
func (f *NetworkFormatter) FormatResourceList(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No networks found in the specified namespace(s)."
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d network(s):\n\n", len(list.Items)))
// Group networks by namespace
networksByNamespace := make(map[string][]unstructured.Unstructured)
for _, item := range list.Items {
namespace := item.GetNamespace()
networksByNamespace[namespace] = append(networksByNamespace[namespace], item)
}
// Print networks grouped by namespace
for namespace, networks := range networksByNamespace {
sb.WriteString(fmt.Sprintf("Namespace: %s (%d networks)\n", namespace, len(networks)))
for _, network := range networks {
// Get type and config
networkType := getNestedString(network.Object, "spec", "type")
// Basic network info
sb.WriteString(fmt.Sprintf(" • %s\n", network.GetName()))
if networkType != "" {
sb.WriteString(fmt.Sprintf(" Type: %s\n", networkType))
}
// VLAN ID if present
vlanId := getNestedInt64(network.Object, "spec", "config", "vlan")
if vlanId > 0 {
sb.WriteString(fmt.Sprintf(" VLAN ID: %d\n", vlanId))
}
// Creation time
creationTime := network.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
// VMImageFormatter handles formatting for VirtualMachineImage resources
type VMImageFormatter struct{}
func (f *VMImageFormatter) FormatResource(res *unstructured.Unstructured) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("VM Image: %s\n", res.GetName()))
sb.WriteString(fmt.Sprintf("Namespace: %s\n", res.GetNamespace()))
// Get image details
displayName := getNestedString(res.Object, "spec", "displayName")
url := getNestedString(res.Object, "spec", "url")
description := getNestedString(res.Object, "spec", "description")
if displayName != "" {
sb.WriteString(fmt.Sprintf("Display Name: %s\n", displayName))
}
if url != "" {
sb.WriteString(fmt.Sprintf("URL: %s\n", url))
}
if description != "" {
sb.WriteString(fmt.Sprintf("Description: %s\n", description))
}
// Status details
status, statusFound, _ := unstructured.NestedMap(res.Object, "status")
if statusFound && len(status) > 0 {
sb.WriteString("\nStatus:\n")
state := getNestedString(res.Object, "status", "state")
if state != "" {
sb.WriteString(fmt.Sprintf(" State: %s\n", state))
}
progress := getNestedString(res.Object, "status", "progress")
if progress != "" {
sb.WriteString(fmt.Sprintf(" Progress: %s\n", progress))
}
size := getNestedString(res.Object, "status", "size")
if size != "" {
sb.WriteString(fmt.Sprintf(" Size: %s\n", size))
}
}
// Creation time
creationTime := res.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("\nCreated: %s\n", creationTime))
return sb.String()
}
func (f *VMImageFormatter) FormatResourceList(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No VM images found in the specified namespace(s)."
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d VM image(s):\n\n", len(list.Items)))
// Group images by namespace
imagesByNamespace := make(map[string][]unstructured.Unstructured)
for _, item := range list.Items {
namespace := item.GetNamespace()
imagesByNamespace[namespace] = append(imagesByNamespace[namespace], item)
}
// Print images grouped by namespace
for namespace, images := range imagesByNamespace {
sb.WriteString(fmt.Sprintf("Namespace: %s (%d images)\n", namespace, len(images)))
for _, image := range images {
// Get basic info
url := getNestedString(image.Object, "spec", "displayName")
if url == "" {
url = getNestedString(image.Object, "spec", "url")
}
size := getNestedString(image.Object, "status", "size")
progress := getNestedString(image.Object, "status", "progress")
// Basic image info
sb.WriteString(fmt.Sprintf(" • %s\n", image.GetName()))
if url != "" {
sb.WriteString(fmt.Sprintf(" Source: %s\n", url))
}
if size != "" {
sb.WriteString(fmt.Sprintf(" Size: %s\n", size))
}
if progress != "" {
sb.WriteString(fmt.Sprintf(" Progress: %s\n", progress))
}
// Creation time
creationTime := image.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
// CRDFormatter handles formatting for CustomResourceDefinition resources
type CRDFormatter struct{}
func (f *CRDFormatter) FormatResource(res *unstructured.Unstructured) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Custom Resource Definition: %s\n", res.GetName()))
// Get CRD details
group := getNestedString(res.Object, "spec", "group")
kind := getNestedString(res.Object, "spec", "names", "kind")
plural := getNestedString(res.Object, "spec", "names", "plural")
scope := getNestedString(res.Object, "spec", "scope")
sb.WriteString(fmt.Sprintf("Group: %s\n", group))
sb.WriteString(fmt.Sprintf("Kind: %s\n", kind))
sb.WriteString(fmt.Sprintf("Plural: %s\n", plural))
sb.WriteString(fmt.Sprintf("Scope: %s\n", scope))
// Short names
shortNames, _, _ := unstructured.NestedStringSlice(res.Object, "spec", "names", "shortNames")
if len(shortNames) > 0 {
sb.WriteString("Short Names: ")
for i, name := range shortNames {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(name)
}
sb.WriteString("\n")
}
// Versions
versions, _, _ := unstructured.NestedSlice(res.Object, "spec", "versions")
if len(versions) > 0 {
sb.WriteString("\nVersions:\n")
for _, versionObj := range versions {
version, ok := versionObj.(map[string]interface{})
if !ok {
continue
}
name, _, _ := unstructured.NestedString(version, "name")
served, _, _ := unstructured.NestedBool(version, "served")
storage, _, _ := unstructured.NestedBool(version, "storage")
sb.WriteString(fmt.Sprintf(" %s:\n", name))
sb.WriteString(fmt.Sprintf(" Served: %t\n", served))
sb.WriteString(fmt.Sprintf(" Storage: %t\n", storage))
// Schema details if present
schema, schemaFound, _ := unstructured.NestedMap(version, "schema", "openAPIV3Schema")
if schemaFound && len(schema) > 0 {
sb.WriteString(" Schema: Available\n")
}
}
}
// Creation time
creationTime := res.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("\nCreated: %s\n", creationTime))
return sb.String()
}
func (f *CRDFormatter) FormatResourceList(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No custom resource definitions found."
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d custom resource definition(s):\n\n", len(list.Items)))
// Group CRDs by group
crdsByGroup := make(map[string][]unstructured.Unstructured)
for _, item := range list.Items {
group := getNestedString(item.Object, "spec", "group")
if group == "" {
group = "core"
}
crdsByGroup[group] = append(crdsByGroup[group], item)
}
// Print CRDs grouped by group
for group, crds := range crdsByGroup {
sb.WriteString(fmt.Sprintf("Group: %s (%d CRDs)\n", group, len(crds)))
for _, crd := range crds {
// Get basic info
kind := getNestedString(crd.Object, "spec", "names", "kind")
plural := getNestedString(crd.Object, "spec", "names", "plural")
// Basic CRD info
sb.WriteString(fmt.Sprintf(" • %s\n", crd.GetName()))
sb.WriteString(fmt.Sprintf(" Kind: %s\n", kind))
sb.WriteString(fmt.Sprintf(" Plural: %s\n", plural))
// Versions
versions, _, _ := unstructured.NestedSlice(crd.Object, "spec", "versions")
if len(versions) > 0 {
sb.WriteString(" Versions:\n")
for _, versionObj := range versions {
version, ok := versionObj.(map[string]interface{})
if !ok {
continue
}
name, _, _ := unstructured.NestedString(version, "name")
served, _, _ := unstructured.NestedBool(version, "served")
storage, _, _ := unstructured.NestedBool(version, "storage")
sb.WriteString(fmt.Sprintf(" %s (served: %t, storage: %t)\n", name, served, storage))
}
}
// Creation time
creationTime := crd.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
```
--------------------------------------------------------------------------------
/pkg/mcp/server.go:
--------------------------------------------------------------------------------
```go
package mcp
import (
"context"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
log "github.com/sirupsen/logrus"
"github.com/starbops/harvester-mcp-server/pkg/client"
"github.com/starbops/harvester-mcp-server/pkg/kubernetes"
)
// Config represents the configuration for the Harvester MCP server.
type Config struct {
// KubeConfigPath is the path to the kubeconfig file.
KubeConfigPath string
}
// HarvesterMCPServer represents the MCP server for Harvester HCI.
type HarvesterMCPServer struct {
mcpServer *server.MCPServer
k8sClient *client.Client
resourceHandler *kubernetes.ResourceHandler
}
// NewServer creates a new Harvester MCP server.
func NewServer(cfg *Config) (*HarvesterMCPServer, error) {
// Create client configuration
clientCfg := &client.Config{
KubeConfigPath: cfg.KubeConfigPath,
}
// Create Kubernetes client
k8sClient, err := client.NewClient(clientCfg)
if err != nil {
return nil, fmt.Errorf("failed to create Kubernetes client: %w", err)
}
// Create resource handler
resourceHandler, err := kubernetes.NewResourceHandler(k8sClient)
if err != nil {
return nil, fmt.Errorf("failed to create resource handler: %w", err)
}
// Create a new MCP server
mcpServer := server.NewMCPServer(
"Harvester MCP Server",
"1.0.0",
)
harvesterServer := &HarvesterMCPServer{
mcpServer: mcpServer,
k8sClient: k8sClient,
resourceHandler: resourceHandler,
}
// Register tools
harvesterServer.registerTools()
return harvesterServer, nil
}
// ServeStdio starts the MCP server using stdio.
func (s *HarvesterMCPServer) ServeStdio() error {
log.Info("Starting Harvester MCP server...")
return server.ServeStdio(s.mcpServer)
}
// registerTools registers all the tools with the MCP server.
func (s *HarvesterMCPServer) registerTools() {
// Register Kubernetes common tools
s.registerKubernetesPodTools()
s.registerKubernetesDeploymentTools()
s.registerKubernetesServiceTools()
s.registerKubernetesNamespaceTools()
s.registerKubernetesNodeTools()
s.registerKubernetesCRDTools()
// Register Harvester-specific tools
s.registerHarvesterVirtualMachineTools()
s.registerHarvesterImageTools()
s.registerHarvesterVolumeTools()
s.registerHarvesterNetworkTools()
}
// registerKubernetesPodTools registers Pod-related tools.
func (s *HarvesterMCPServer) registerKubernetesPodTools() {
// List pods tool
listPodsTool := mcp.NewTool(
"list_pods",
mcp.WithDescription("List pods in the Harvester cluster"),
mcp.WithString("namespace",
mcp.Description("The namespace to list pods from (optional, defaults to all namespaces)"),
),
)
s.mcpServer.AddTool(listPodsTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, _ := req.Params.Arguments["namespace"].(string)
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypePods]
list, err := s.resourceHandler.ListResources(ctx, gvr, namespace)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list pods: %v", err)), nil
}
// Format the list using the resource formatter
formatted := s.resourceHandler.FormatResourceList(list, gvr)
return mcp.NewToolResultText(formatted), nil
})
// Get pod tool
getPodTool := mcp.NewTool(
"get_pod",
mcp.WithDescription("Get pod details from the Harvester cluster"),
mcp.WithString("namespace",
mcp.Required(),
mcp.Description("The namespace of the pod"),
),
mcp.WithString("name",
mcp.Required(),
mcp.Description("The name of the pod"),
),
)
s.mcpServer.AddTool(getPodTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, ok := req.Params.Arguments["namespace"].(string)
if !ok || namespace == "" {
return mcp.NewToolResultError("Namespace is required"), nil
}
name, ok := req.Params.Arguments["name"].(string)
if !ok || name == "" {
return mcp.NewToolResultError("Pod name is required"), nil
}
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypePod]
resource, err := s.resourceHandler.GetResource(ctx, gvr, namespace, name)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to get pod %s in namespace %s: %v", name, namespace, err)), nil
}
// Format the resource using the resource formatter
formatted := s.resourceHandler.FormatResource(resource, gvr)
return mcp.NewToolResultText(formatted), nil
})
// Delete pod tool
deletePodTool := mcp.NewTool(
"delete_pod",
mcp.WithDescription("Delete a pod from the Harvester cluster"),
mcp.WithString("namespace",
mcp.Required(),
mcp.Description("The namespace of the pod"),
),
mcp.WithString("name",
mcp.Required(),
mcp.Description("The name of the pod to delete"),
),
)
s.mcpServer.AddTool(deletePodTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, ok := req.Params.Arguments["namespace"].(string)
if !ok || namespace == "" {
return mcp.NewToolResultError("Namespace is required"), nil
}
name, ok := req.Params.Arguments["name"].(string)
if !ok || name == "" {
return mcp.NewToolResultError("Pod name is required"), nil
}
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypePod]
err := s.resourceHandler.DeleteResource(ctx, gvr, namespace, name)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to delete pod %s in namespace %s: %v", name, namespace, err)), nil
}
return mcp.NewToolResultText(fmt.Sprintf("Pod %s in namespace %s deleted successfully", name, namespace)), nil
})
}
// registerKubernetesDeploymentTools registers Deployment-related tools.
func (s *HarvesterMCPServer) registerKubernetesDeploymentTools() {
// List deployments tool
listDeploymentsTool := mcp.NewTool(
"list_deployments",
mcp.WithDescription("List deployments in the Harvester cluster"),
mcp.WithString("namespace",
mcp.Description("The namespace to list deployments from (optional, defaults to all namespaces)"),
),
)
s.mcpServer.AddTool(listDeploymentsTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, _ := req.Params.Arguments["namespace"].(string)
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeDeployments]
list, err := s.resourceHandler.ListResources(ctx, gvr, namespace)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list deployments: %v", err)), nil
}
// Format the list using the resource formatter
formatted := s.resourceHandler.FormatResourceList(list, gvr)
return mcp.NewToolResultText(formatted), nil
})
// Get deployment tool
getDeploymentTool := mcp.NewTool(
"get_deployment",
mcp.WithDescription("Get deployment details from the Harvester cluster"),
mcp.WithString("namespace",
mcp.Required(),
mcp.Description("The namespace of the deployment"),
),
mcp.WithString("name",
mcp.Required(),
mcp.Description("The name of the deployment"),
),
)
s.mcpServer.AddTool(getDeploymentTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, ok := req.Params.Arguments["namespace"].(string)
if !ok || namespace == "" {
return mcp.NewToolResultError("Namespace is required"), nil
}
name, ok := req.Params.Arguments["name"].(string)
if !ok || name == "" {
return mcp.NewToolResultError("Deployment name is required"), nil
}
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeDeployment]
resource, err := s.resourceHandler.GetResource(ctx, gvr, namespace, name)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to get deployment %s in namespace %s: %v", name, namespace, err)), nil
}
// Format the resource using the resource formatter
formatted := s.resourceHandler.FormatResource(resource, gvr)
return mcp.NewToolResultText(formatted), nil
})
}
// registerKubernetesServiceTools registers Service-related tools.
func (s *HarvesterMCPServer) registerKubernetesServiceTools() {
// List services tool
listServicesTool := mcp.NewTool(
"list_services",
mcp.WithDescription("List services in the Harvester cluster"),
mcp.WithString("namespace",
mcp.Description("The namespace to list services from (optional, defaults to all namespaces)"),
),
)
s.mcpServer.AddTool(listServicesTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, _ := req.Params.Arguments["namespace"].(string)
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeServices]
list, err := s.resourceHandler.ListResources(ctx, gvr, namespace)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list services: %v", err)), nil
}
// Format the list using the resource formatter
formatted := s.resourceHandler.FormatResourceList(list, gvr)
return mcp.NewToolResultText(formatted), nil
})
// Get service tool
getServiceTool := mcp.NewTool(
"get_service",
mcp.WithDescription("Get service details from the Harvester cluster"),
mcp.WithString("namespace",
mcp.Required(),
mcp.Description("The namespace of the service"),
),
mcp.WithString("name",
mcp.Required(),
mcp.Description("The name of the service"),
),
)
s.mcpServer.AddTool(getServiceTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, ok := req.Params.Arguments["namespace"].(string)
if !ok || namespace == "" {
return mcp.NewToolResultError("Namespace is required"), nil
}
name, ok := req.Params.Arguments["name"].(string)
if !ok || name == "" {
return mcp.NewToolResultError("Service name is required"), nil
}
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeService]
resource, err := s.resourceHandler.GetResource(ctx, gvr, namespace, name)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to get service %s in namespace %s: %v", name, namespace, err)), nil
}
// Format the resource using the resource formatter
formatted := s.resourceHandler.FormatResource(resource, gvr)
return mcp.NewToolResultText(formatted), nil
})
}
// registerKubernetesNamespaceTools registers Namespace-related tools.
func (s *HarvesterMCPServer) registerKubernetesNamespaceTools() {
// List namespaces tool
listNamespacesTool := mcp.NewTool(
"list_namespaces",
mcp.WithDescription("List namespaces in the Harvester cluster"),
)
s.mcpServer.AddTool(listNamespacesTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeNamespaces]
list, err := s.resourceHandler.ListResources(ctx, gvr, "")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list namespaces: %v", err)), nil
}
// Format the list using the resource formatter
formatted := s.resourceHandler.FormatResourceList(list, gvr)
return mcp.NewToolResultText(formatted), nil
})
// Get namespace tool
getNamespaceTool := mcp.NewTool(
"get_namespace",
mcp.WithDescription("Get namespace details from the Harvester cluster"),
mcp.WithString("name",
mcp.Required(),
mcp.Description("The name of the namespace"),
),
)
s.mcpServer.AddTool(getNamespaceTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, ok := req.Params.Arguments["name"].(string)
if !ok || name == "" {
return mcp.NewToolResultError("Namespace name is required"), nil
}
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeNamespace]
resource, err := s.resourceHandler.GetResource(ctx, gvr, "", name)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace %s: %v", name, err)), nil
}
// Format the resource using the resource formatter
formatted := s.resourceHandler.FormatResource(resource, gvr)
return mcp.NewToolResultText(formatted), nil
})
}
// registerKubernetesNodeTools registers Node-related tools.
func (s *HarvesterMCPServer) registerKubernetesNodeTools() {
// List nodes tool
listNodesTool := mcp.NewTool(
"list_nodes",
mcp.WithDescription("List nodes in the Harvester cluster"),
)
s.mcpServer.AddTool(listNodesTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeNodes]
list, err := s.resourceHandler.ListResources(ctx, gvr, "")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list nodes: %v", err)), nil
}
// Format the list using the resource formatter
formatted := s.resourceHandler.FormatResourceList(list, gvr)
return mcp.NewToolResultText(formatted), nil
})
// Get node tool
getNodeTool := mcp.NewTool(
"get_node",
mcp.WithDescription("Get node details from the Harvester cluster"),
mcp.WithString("name",
mcp.Required(),
mcp.Description("The name of the node"),
),
)
s.mcpServer.AddTool(getNodeTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, ok := req.Params.Arguments["name"].(string)
if !ok || name == "" {
return mcp.NewToolResultError("Node name is required"), nil
}
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeNode]
resource, err := s.resourceHandler.GetResource(ctx, gvr, "", name)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to get node %s: %v", name, err)), nil
}
// Format the resource using the resource formatter
formatted := s.resourceHandler.FormatResource(resource, gvr)
return mcp.NewToolResultText(formatted), nil
})
}
// registerKubernetesCRDTools registers CRD-related tools.
func (s *HarvesterMCPServer) registerKubernetesCRDTools() {
// List CRDs tool
listCRDsTool := mcp.NewTool(
"list_crds",
mcp.WithDescription("List Custom Resource Definitions in the Harvester cluster"),
)
s.mcpServer.AddTool(listCRDsTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeCRDs]
list, err := s.resourceHandler.ListResources(ctx, gvr, "")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list CRDs: %v", err)), nil
}
// Format the list using the resource formatter
formatted := s.resourceHandler.FormatResourceList(list, gvr)
return mcp.NewToolResultText(formatted), nil
})
}
// registerHarvesterVirtualMachineTools registers Harvester VM-related tools.
func (s *HarvesterMCPServer) registerHarvesterVirtualMachineTools() {
// List VMs tool
listVMsTool := mcp.NewTool(
"list_vms",
mcp.WithDescription("List Virtual Machines in the Harvester cluster"),
mcp.WithString("namespace",
mcp.Description("The namespace to list VMs from (optional, defaults to all namespaces)"),
),
)
s.mcpServer.AddTool(listVMsTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, _ := req.Params.Arguments["namespace"].(string)
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeVMs]
list, err := s.resourceHandler.ListResources(ctx, gvr, namespace)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list VMs: %v", err)), nil
}
// Format the list using the resource formatter
formatted := s.resourceHandler.FormatResourceList(list, gvr)
return mcp.NewToolResultText(formatted), nil
})
// Get VM tool
getVMTool := mcp.NewTool(
"get_vm",
mcp.WithDescription("Get Virtual Machine details from the Harvester cluster"),
mcp.WithString("namespace",
mcp.Required(),
mcp.Description("The namespace of the VM"),
),
mcp.WithString("name",
mcp.Required(),
mcp.Description("The name of the VM"),
),
)
s.mcpServer.AddTool(getVMTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, ok := req.Params.Arguments["namespace"].(string)
if !ok || namespace == "" {
return mcp.NewToolResultError("Namespace is required"), nil
}
name, ok := req.Params.Arguments["name"].(string)
if !ok || name == "" {
return mcp.NewToolResultError("VM name is required"), nil
}
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeVM]
resource, err := s.resourceHandler.GetResource(ctx, gvr, namespace, name)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to get VM %s in namespace %s: %v", name, namespace, err)), nil
}
// Format the resource using the resource formatter
formatted := s.resourceHandler.FormatResource(resource, gvr)
return mcp.NewToolResultText(formatted), nil
})
}
// registerHarvesterImageTools registers Harvester Image-related tools.
func (s *HarvesterMCPServer) registerHarvesterImageTools() {
// List images tool
listImagesTool := mcp.NewTool(
"list_images",
mcp.WithDescription("List Images in the Harvester cluster"),
mcp.WithString("namespace",
mcp.Description("The namespace to list images from (optional, defaults to all namespaces)"),
),
)
s.mcpServer.AddTool(listImagesTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, _ := req.Params.Arguments["namespace"].(string)
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeImages]
list, err := s.resourceHandler.ListResources(ctx, gvr, namespace)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list images: %v", err)), nil
}
// Format the list using the resource formatter
formatted := s.resourceHandler.FormatResourceList(list, gvr)
return mcp.NewToolResultText(formatted), nil
})
}
// registerHarvesterVolumeTools registers Harvester Volume-related tools.
func (s *HarvesterMCPServer) registerHarvesterVolumeTools() {
// List volumes tool
listVolumesTool := mcp.NewTool(
"list_volumes",
mcp.WithDescription("List Volumes in the Harvester cluster"),
mcp.WithString("namespace",
mcp.Description("The namespace to list volumes from (optional, defaults to all namespaces)"),
),
)
s.mcpServer.AddTool(listVolumesTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, _ := req.Params.Arguments["namespace"].(string)
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeVolumes]
list, err := s.resourceHandler.ListResources(ctx, gvr, namespace)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list volumes: %v", err)), nil
}
// Format the list using the resource formatter
formatted := s.resourceHandler.FormatResourceList(list, gvr)
return mcp.NewToolResultText(formatted), nil
})
}
// registerHarvesterNetworkTools registers Harvester Network-related tools.
func (s *HarvesterMCPServer) registerHarvesterNetworkTools() {
// List networks tool
listNetworksTool := mcp.NewTool(
"list_networks",
mcp.WithDescription("List Networks in the Harvester cluster"),
mcp.WithString("namespace",
mcp.Description("The namespace to list networks from (optional, defaults to all namespaces)"),
),
)
s.mcpServer.AddTool(listNetworksTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, _ := req.Params.Arguments["namespace"].(string)
// Use the unified resource handler
gvr := kubernetes.ResourceTypeToGVR[kubernetes.ResourceTypeNetworks]
list, err := s.resourceHandler.ListResources(ctx, gvr, namespace)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list networks: %v", err)), nil
}
// Format the list using the resource formatter
formatted := s.resourceHandler.FormatResourceList(list, gvr)
return mcp.NewToolResultText(formatted), nil
})
}
```
--------------------------------------------------------------------------------
/pkg/kubernetes/core_formatters.go:
--------------------------------------------------------------------------------
```go
package kubernetes
import (
"fmt"
"strings"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// PodFormatter handles formatting for Pod resources
type PodFormatter struct{}
func (f *PodFormatter) FormatResource(res *unstructured.Unstructured) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Pod: %s\n", res.GetName()))
sb.WriteString(fmt.Sprintf("Namespace: %s\n", res.GetNamespace()))
// Status
status := getNestedString(res.Object, "status", "phase")
reason := getNestedString(res.Object, "status", "reason")
if reason != "" {
status = reason
}
sb.WriteString(fmt.Sprintf("Status: %s\n", status))
// Node
nodeName := getNestedString(res.Object, "spec", "nodeName")
if nodeName != "" {
sb.WriteString(fmt.Sprintf("Node: %s\n", nodeName))
}
// IP addresses
podIP := getNestedString(res.Object, "status", "podIP")
if podIP != "" {
sb.WriteString(fmt.Sprintf("Pod IP: %s\n", podIP))
}
// Creation time
creationTime := res.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("Created: %s\n", creationTime))
// QoS Class
qosClass := getNestedString(res.Object, "status", "qosClass")
sb.WriteString(fmt.Sprintf("QoS Class: %s\n", qosClass))
// Labels
if labels := res.GetLabels(); len(labels) > 0 {
sb.WriteString("\nLabels:\n")
for key, value := range labels {
sb.WriteString(fmt.Sprintf(" %s: %s\n", key, value))
}
}
// Containers
containers, _, _ := unstructured.NestedSlice(res.Object, "spec", "containers")
if len(containers) > 0 {
sb.WriteString("\nContainers:\n")
for i, containerObj := range containers {
container, ok := containerObj.(map[string]interface{})
if !ok {
continue
}
name, _, _ := unstructured.NestedString(container, "name")
image, _, _ := unstructured.NestedString(container, "image")
sb.WriteString(fmt.Sprintf(" %d. %s\n", i+1, name))
sb.WriteString(fmt.Sprintf(" Image: %s\n", image))
// Container resources
resources, found, _ := unstructured.NestedMap(container, "resources")
if found {
sb.WriteString(" Resources:\n")
limits, limitsFound, _ := unstructured.NestedMap(resources, "limits")
if limitsFound {
for resource, value := range limits {
sb.WriteString(fmt.Sprintf(" Limits %s: %v\n", resource, value))
}
}
requests, requestsFound, _ := unstructured.NestedMap(resources, "requests")
if requestsFound {
for resource, value := range requests {
sb.WriteString(fmt.Sprintf(" Requests %s: %v\n", resource, value))
}
}
}
sb.WriteString("\n")
}
}
return sb.String()
}
func (f *PodFormatter) FormatResourceList(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No pods found in the specified namespace(s)."
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d pod(s):\n\n", len(list.Items)))
// Group pods by namespace
podsByNamespace := make(map[string][]unstructured.Unstructured)
for _, item := range list.Items {
namespace := item.GetNamespace()
podsByNamespace[namespace] = append(podsByNamespace[namespace], item)
}
// Print pods grouped by namespace
for namespace, pods := range podsByNamespace {
sb.WriteString(fmt.Sprintf("Namespace: %s (%d pods)\n", namespace, len(pods)))
for _, pod := range pods {
// Get pod status
status := getNestedString(pod.Object, "status", "phase")
reason := getNestedString(pod.Object, "status", "reason")
if reason != "" {
status = reason
}
// Add ready container count
ready := 0
containerStatuses, _, _ := unstructured.NestedSlice(pod.Object, "status", "containerStatuses")
for _, csObj := range containerStatuses {
cs, ok := csObj.(map[string]interface{})
if ok {
isReady, found, _ := unstructured.NestedBool(cs, "ready")
if found && isReady {
ready++
}
}
}
containers, _, _ := unstructured.NestedSlice(pod.Object, "spec", "containers")
// Basic pod info
sb.WriteString(fmt.Sprintf(" • %s\n", pod.GetName()))
sb.WriteString(fmt.Sprintf(" Status: %s\n", status))
sb.WriteString(fmt.Sprintf(" Ready: %d/%d containers\n", ready, len(containers)))
// Add node name if scheduled
nodeName := getNestedString(pod.Object, "spec", "nodeName")
if nodeName != "" {
sb.WriteString(fmt.Sprintf(" Node: %s\n", nodeName))
}
// Add IP if assigned
podIP := getNestedString(pod.Object, "status", "podIP")
if podIP != "" {
sb.WriteString(fmt.Sprintf(" IP: %s\n", podIP))
}
// Add creation time
creationTime := pod.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
// Add restart count
totalRestarts := 0
for _, csObj := range containerStatuses {
cs, ok := csObj.(map[string]interface{})
if ok {
restarts, found, _ := unstructured.NestedInt64(cs, "restartCount")
if found {
totalRestarts += int(restarts)
}
}
}
sb.WriteString(fmt.Sprintf(" Restarts: %d\n", totalRestarts))
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
// ServiceFormatter handles formatting for Service resources
type ServiceFormatter struct{}
func (f *ServiceFormatter) FormatResource(res *unstructured.Unstructured) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Service: %s\n", res.GetName()))
sb.WriteString(fmt.Sprintf("Namespace: %s\n", res.GetNamespace()))
// Type
svcType := getNestedString(res.Object, "spec", "type")
sb.WriteString(fmt.Sprintf("Type: %s\n", svcType))
// ClusterIP
clusterIP := getNestedString(res.Object, "spec", "clusterIP")
sb.WriteString(fmt.Sprintf("Cluster IP: %s\n", clusterIP))
// External IPs
externalIPs, _, _ := unstructured.NestedStringSlice(res.Object, "spec", "externalIPs")
if len(externalIPs) > 0 {
sb.WriteString("External IPs:\n")
for _, ip := range externalIPs {
sb.WriteString(fmt.Sprintf(" %s\n", ip))
}
}
// Selectors
selector, selectorFound, _ := unstructured.NestedMap(res.Object, "spec", "selector")
if selectorFound && len(selector) > 0 {
sb.WriteString("\nSelector:\n")
for key, value := range selector {
sb.WriteString(fmt.Sprintf(" %s: %v\n", key, value))
}
}
// Ports
ports, _, _ := unstructured.NestedSlice(res.Object, "spec", "ports")
if len(ports) > 0 {
sb.WriteString("\nPorts:\n")
for _, portObj := range ports {
port, ok := portObj.(map[string]interface{})
if !ok {
continue
}
portNumber, _, _ := unstructured.NestedInt64(port, "port")
targetPort, _, _ := unstructured.NestedFieldNoCopy(port, "targetPort")
protocol, _, _ := unstructured.NestedString(port, "protocol")
name, _, _ := unstructured.NestedString(port, "name")
if name != "" {
sb.WriteString(fmt.Sprintf(" %s:\n", name))
sb.WriteString(fmt.Sprintf(" Port: %d\n", portNumber))
sb.WriteString(fmt.Sprintf(" Target Port: %v\n", targetPort))
sb.WriteString(fmt.Sprintf(" Protocol: %s\n", protocol))
} else {
sb.WriteString(fmt.Sprintf(" Port: %d\n", portNumber))
sb.WriteString(fmt.Sprintf(" Target Port: %v\n", targetPort))
sb.WriteString(fmt.Sprintf(" Protocol: %s\n", protocol))
}
sb.WriteString("\n")
}
}
// Creation time
creationTime := res.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("Created: %s\n", creationTime))
return sb.String()
}
func (f *ServiceFormatter) FormatResourceList(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No services found in the specified namespace(s)."
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d service(s):\n\n", len(list.Items)))
// Group services by namespace
servicesByNamespace := make(map[string][]unstructured.Unstructured)
for _, item := range list.Items {
namespace := item.GetNamespace()
servicesByNamespace[namespace] = append(servicesByNamespace[namespace], item)
}
// Print services grouped by namespace
for namespace, services := range servicesByNamespace {
sb.WriteString(fmt.Sprintf("Namespace: %s (%d services)\n", namespace, len(services)))
for _, svc := range services {
// Type
svcType := getNestedString(svc.Object, "spec", "type")
// ClusterIP
clusterIP := getNestedString(svc.Object, "spec", "clusterIP")
// Ports
ports, _, _ := unstructured.NestedSlice(svc.Object, "spec", "ports")
// Basic service info
sb.WriteString(fmt.Sprintf(" • %s\n", svc.GetName()))
sb.WriteString(fmt.Sprintf(" Type: %s\n", svcType))
sb.WriteString(fmt.Sprintf(" Cluster IP: %s\n", clusterIP))
if len(ports) > 0 {
sb.WriteString(" Ports:\n")
for _, portObj := range ports {
port, ok := portObj.(map[string]interface{})
if !ok {
continue
}
portNumber, _, _ := unstructured.NestedInt64(port, "port")
targetPort, _, _ := unstructured.NestedFieldNoCopy(port, "targetPort")
protocol, _, _ := unstructured.NestedString(port, "protocol")
name, _, _ := unstructured.NestedString(port, "name")
portInfo := fmt.Sprintf("%d", portNumber)
if name != "" {
portInfo = fmt.Sprintf("%s:%d", name, portNumber)
}
sb.WriteString(fmt.Sprintf(" %s → %v/%s\n", portInfo, targetPort, protocol))
}
}
// External IP if available
externalIPs, _, _ := unstructured.NestedStringSlice(svc.Object, "spec", "externalIPs")
if len(externalIPs) > 0 {
sb.WriteString(" External IPs:\n")
for _, ip := range externalIPs {
sb.WriteString(fmt.Sprintf(" %s\n", ip))
}
}
// Creation time
creationTime := svc.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
// NamespaceFormatter handles formatting for Namespace resources
type NamespaceFormatter struct{}
func (f *NamespaceFormatter) FormatResource(res *unstructured.Unstructured) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Namespace: %s\n", res.GetName()))
// Status
status := getNestedString(res.Object, "status", "phase")
sb.WriteString(fmt.Sprintf("Status: %s\n", status))
// Creation time
creationTime := res.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("Created: %s\n", creationTime))
// Labels
if labels := res.GetLabels(); len(labels) > 0 {
sb.WriteString("\nLabels:\n")
for key, value := range labels {
sb.WriteString(fmt.Sprintf(" %s: %s\n", key, value))
}
}
// Annotations
if annotations := res.GetAnnotations(); len(annotations) > 0 {
sb.WriteString("\nAnnotations:\n")
for key, value := range annotations {
sb.WriteString(fmt.Sprintf(" %s: %s\n", key, value))
}
}
return sb.String()
}
func (f *NamespaceFormatter) FormatResourceList(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No namespaces found."
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d namespace(s):\n\n", len(list.Items)))
for _, ns := range list.Items {
// Get status
status := getNestedString(ns.Object, "status", "phase")
// Basic namespace info
sb.WriteString(fmt.Sprintf("• %s\n", ns.GetName()))
sb.WriteString(fmt.Sprintf(" Status: %s\n", status))
// Creation time
creationTime := ns.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
sb.WriteString("\n")
}
return sb.String()
}
// NodeFormatter handles formatting for Node resources
type NodeFormatter struct{}
func (f *NodeFormatter) FormatResource(res *unstructured.Unstructured) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Node: %s\n", res.GetName()))
// Node status
status := "Unknown"
conditions, _, _ := unstructured.NestedSlice(res.Object, "status", "conditions")
for _, condObj := range conditions {
cond, ok := condObj.(map[string]interface{})
if !ok {
continue
}
typeName, typeFound, _ := unstructured.NestedString(cond, "type")
statusVal, statusFound, _ := unstructured.NestedString(cond, "status")
if typeFound && statusFound && typeName == "Ready" {
if statusVal == "True" {
status = "Ready"
} else {
status = "NotReady"
}
break
}
}
sb.WriteString(fmt.Sprintf("Status: %s\n", status))
// Detailed conditions
sb.WriteString("\nConditions:\n")
for _, condObj := range conditions {
cond, ok := condObj.(map[string]interface{})
if !ok {
continue
}
typeName, _, _ := unstructured.NestedString(cond, "type")
statusVal, _, _ := unstructured.NestedString(cond, "status")
message, _, _ := unstructured.NestedString(cond, "message")
sb.WriteString(fmt.Sprintf(" %s: %s\n", typeName, statusVal))
if message != "" {
sb.WriteString(fmt.Sprintf(" Message: %s\n", message))
}
}
// Addresses
addresses, _, _ := unstructured.NestedSlice(res.Object, "status", "addresses")
if len(addresses) > 0 {
sb.WriteString("\nAddresses:\n")
for _, addrObj := range addresses {
addr, ok := addrObj.(map[string]interface{})
if !ok {
continue
}
addrType, typeFound, _ := unstructured.NestedString(addr, "type")
addrVal, valFound, _ := unstructured.NestedString(addr, "address")
if typeFound && valFound {
sb.WriteString(fmt.Sprintf(" %s: %s\n", addrType, addrVal))
}
}
}
// Node info
nodeInfo, _, _ := unstructured.NestedMap(res.Object, "status", "nodeInfo")
if len(nodeInfo) > 0 {
sb.WriteString("\nNode Info:\n")
for key, value := range nodeInfo {
sb.WriteString(fmt.Sprintf(" %s: %v\n", key, value))
}
}
// Resources
allocatable, _, _ := unstructured.NestedMap(res.Object, "status", "allocatable")
capacity, _, _ := unstructured.NestedMap(res.Object, "status", "capacity")
if len(allocatable) > 0 {
sb.WriteString("\nAllocatable Resources:\n")
for key, value := range allocatable {
sb.WriteString(fmt.Sprintf(" %s: %v\n", key, value))
}
}
if len(capacity) > 0 {
sb.WriteString("\nCapacity:\n")
for key, value := range capacity {
sb.WriteString(fmt.Sprintf(" %s: %v\n", key, value))
}
}
// Creation time
creationTime := res.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("\nCreated: %s\n", creationTime))
return sb.String()
}
func (f *NodeFormatter) FormatResourceList(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No nodes found."
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d node(s):\n\n", len(list.Items)))
for _, node := range list.Items {
// Get node status
status := "Ready"
conditions, _, _ := unstructured.NestedSlice(node.Object, "status", "conditions")
for _, condObj := range conditions {
cond, ok := condObj.(map[string]interface{})
if !ok {
continue
}
typeName, typeFound, _ := unstructured.NestedString(cond, "type")
status, statusFound, _ := unstructured.NestedString(cond, "status")
if typeFound && statusFound && typeName == "Ready" {
if status == "True" {
status = "Ready"
} else {
status = "NotReady"
}
break
}
}
// Get addresses
var internalIP, externalIP, hostname string
addresses, _, _ := unstructured.NestedSlice(node.Object, "status", "addresses")
for _, addrObj := range addresses {
addr, ok := addrObj.(map[string]interface{})
if !ok {
continue
}
addrType, typeFound, _ := unstructured.NestedString(addr, "type")
addrVal, valFound, _ := unstructured.NestedString(addr, "address")
if typeFound && valFound {
switch addrType {
case "InternalIP":
internalIP = addrVal
case "ExternalIP":
externalIP = addrVal
case "Hostname":
hostname = addrVal
}
}
}
// Get kubelet version
kubeletVersion := getNestedString(node.Object, "status", "nodeInfo", "kubeletVersion")
// Get allocatable resources
allocatable, _, _ := unstructured.NestedMap(node.Object, "status", "allocatable")
cpu := ""
memory := ""
if allocatable != nil {
if cpuVal, ok := allocatable["cpu"]; ok {
cpu = fmt.Sprintf("%v", cpuVal)
}
if memVal, ok := allocatable["memory"]; ok {
memory = fmt.Sprintf("%v", memVal)
}
}
// Basic node info
sb.WriteString(fmt.Sprintf("• %s\n", node.GetName()))
sb.WriteString(fmt.Sprintf(" Status: %s\n", status))
if internalIP != "" {
sb.WriteString(fmt.Sprintf(" Internal IP: %s\n", internalIP))
}
if externalIP != "" {
sb.WriteString(fmt.Sprintf(" External IP: %s\n", externalIP))
}
if hostname != "" && hostname != node.GetName() {
sb.WriteString(fmt.Sprintf(" Hostname: %s\n", hostname))
}
if kubeletVersion != "" {
sb.WriteString(fmt.Sprintf(" Kubelet Version: %s\n", kubeletVersion))
}
if cpu != "" || memory != "" {
sb.WriteString(" Allocatable:\n")
if cpu != "" {
sb.WriteString(fmt.Sprintf(" CPU: %s\n", cpu))
}
if memory != "" {
sb.WriteString(fmt.Sprintf(" Memory: %s\n", memory))
}
}
// Creation time
creationTime := node.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
sb.WriteString("\n")
}
return sb.String()
}
// DeploymentFormatter handles formatting for Deployment resources
type DeploymentFormatter struct{}
func (f *DeploymentFormatter) FormatResource(res *unstructured.Unstructured) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Deployment: %s\n", res.GetName()))
sb.WriteString(fmt.Sprintf("Namespace: %s\n", res.GetNamespace()))
// Replicas info
replicas := getNestedInt64(res.Object, "spec", "replicas")
available := getNestedInt64(res.Object, "status", "availableReplicas")
ready := getNestedInt64(res.Object, "status", "readyReplicas")
updated := getNestedInt64(res.Object, "status", "updatedReplicas")
sb.WriteString(fmt.Sprintf("Replicas: %d desired | %d updated | %d total | %d available | %d ready\n",
replicas, updated, replicas, available, ready))
// Strategy
strategy := getNestedString(res.Object, "spec", "strategy", "type")
sb.WriteString(fmt.Sprintf("Strategy: %s\n", strategy))
// Selector
selector, selectorFound, _ := unstructured.NestedMap(res.Object, "spec", "selector", "matchLabels")
if selectorFound && len(selector) > 0 {
sb.WriteString("\nSelector:\n")
for key, value := range selector {
sb.WriteString(fmt.Sprintf(" %s: %v\n", key, value))
}
}
// Containers
containers, _, _ := unstructured.NestedSlice(res.Object, "spec", "template", "spec", "containers")
if len(containers) > 0 {
sb.WriteString("\nContainers:\n")
for i, containerObj := range containers {
container, ok := containerObj.(map[string]interface{})
if !ok {
continue
}
name, _, _ := unstructured.NestedString(container, "name")
image, _, _ := unstructured.NestedString(container, "image")
sb.WriteString(fmt.Sprintf(" %d. %s\n", i+1, name))
sb.WriteString(fmt.Sprintf(" Image: %s\n", image))
// Container resources
resources, found, _ := unstructured.NestedMap(container, "resources")
if found {
sb.WriteString(" Resources:\n")
limits, limitsFound, _ := unstructured.NestedMap(resources, "limits")
if limitsFound {
for resource, value := range limits {
sb.WriteString(fmt.Sprintf(" Limits %s: %v\n", resource, value))
}
}
requests, requestsFound, _ := unstructured.NestedMap(resources, "requests")
if requestsFound {
for resource, value := range requests {
sb.WriteString(fmt.Sprintf(" Requests %s: %v\n", resource, value))
}
}
}
sb.WriteString("\n")
}
}
// Conditions
conditions, _, _ := unstructured.NestedSlice(res.Object, "status", "conditions")
if len(conditions) > 0 {
sb.WriteString("\nConditions:\n")
for _, condObj := range conditions {
cond, ok := condObj.(map[string]interface{})
if !ok {
continue
}
typeName, _, _ := unstructured.NestedString(cond, "type")
statusVal, _, _ := unstructured.NestedString(cond, "status")
reason, _, _ := unstructured.NestedString(cond, "reason")
message, _, _ := unstructured.NestedString(cond, "message")
sb.WriteString(fmt.Sprintf(" %s: %s\n", typeName, statusVal))
if reason != "" {
sb.WriteString(fmt.Sprintf(" Reason: %s\n", reason))
}
if message != "" {
sb.WriteString(fmt.Sprintf(" Message: %s\n", message))
}
}
}
// Creation time
creationTime := res.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf("\nCreated: %s\n", creationTime))
return sb.String()
}
func (f *DeploymentFormatter) FormatResourceList(list *unstructured.UnstructuredList) string {
if len(list.Items) == 0 {
return "No deployments found in the specified namespace(s)."
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d deployment(s):\n\n", len(list.Items)))
// Group deployments by namespace
deploymentsByNamespace := make(map[string][]unstructured.Unstructured)
for _, item := range list.Items {
namespace := item.GetNamespace()
deploymentsByNamespace[namespace] = append(deploymentsByNamespace[namespace], item)
}
// Print deployments grouped by namespace
for namespace, deployments := range deploymentsByNamespace {
sb.WriteString(fmt.Sprintf("Namespace: %s (%d deployments)\n", namespace, len(deployments)))
for _, deployment := range deployments {
// Get deployment status
available := getNestedInt64(deployment.Object, "status", "availableReplicas")
replicas := getNestedInt64(deployment.Object, "status", "replicas")
updatedReplicas := getNestedInt64(deployment.Object, "status", "updatedReplicas")
// Basic deployment info
sb.WriteString(fmt.Sprintf(" • %s\n", deployment.GetName()))
sb.WriteString(fmt.Sprintf(" Replicas: %d available / %d total / %d updated\n", available, replicas, updatedReplicas))
// Add image info if available
containers, _, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
if len(containers) > 0 {
sb.WriteString(" Images:\n")
for _, containerObj := range containers {
container, ok := containerObj.(map[string]interface{})
if !ok {
continue
}
name, _, _ := unstructured.NestedString(container, "name")
image, _, _ := unstructured.NestedString(container, "image")
sb.WriteString(fmt.Sprintf(" %s: %s\n", name, image))
}
}
// Status conditions
conditions, _, _ := unstructured.NestedSlice(deployment.Object, "status", "conditions")
if len(conditions) > 0 {
sb.WriteString(" Conditions:\n")
for _, condObj := range conditions {
cond, ok := condObj.(map[string]interface{})
if !ok {
continue
}
typeName, _, _ := unstructured.NestedString(cond, "type")
statusVal, _, _ := unstructured.NestedString(cond, "status")
sb.WriteString(fmt.Sprintf(" %s: %s\n", typeName, statusVal))
}
}
// Creation time
creationTime := deployment.GetCreationTimestamp().Format(time.RFC3339)
sb.WriteString(fmt.Sprintf(" Created: %s\n", creationTime))
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
```