Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 43 additions & 43 deletions cluster/formatted.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,27 +393,27 @@ func formatServiceList(services *corev1.ServiceList, includeNamespace bool) stri

// Format the output
if includeNamespace {
result.WriteString(fmt.Sprintf("• %s/%s: Type=%s, ClusterIP=%s, ExternalIP=%s, Ports=%s, Age=%s\n",
fmt.Fprintf(&result, "• %s/%s: Type=%s, ClusterIP=%s, ExternalIP=%s, Ports=%s, Age=%s\n",
svc.Namespace,
svc.Name,
serviceType,
clusterIP,
externalIP,
portsStr,
formatDuration(age)))
formatDuration(age))
} else {
result.WriteString(fmt.Sprintf("• %s: Type=%s, ClusterIP=%s, ExternalIP=%s, Ports=%s, Age=%s\n",
fmt.Fprintf(&result, "• %s: Type=%s, ClusterIP=%s, ExternalIP=%s, Ports=%s, Age=%s\n",
svc.Name,
serviceType,
clusterIP,
externalIP,
portsStr,
formatDuration(age)))
formatDuration(age))
}
}

// Add total count
result.WriteString(fmt.Sprintf("\nTotal: %d service(s)", len(services.Items)))
fmt.Fprintf(&result, "\nTotal: %d service(s)", len(services.Items))

return result.String()
}
Expand Down Expand Up @@ -612,7 +612,7 @@ func formatNamespaceList(namespaces *corev1.NamespaceList, labelSelector string)
var result strings.Builder

if labelSelector != "" {
result.WriteString(fmt.Sprintf("Namespaces matching label selector '%s':\n", labelSelector))
fmt.Fprintf(&result, "Namespaces matching label selector '%s':\n", labelSelector)
} else {
result.WriteString("Namespaces:\n")
}
Expand All @@ -624,18 +624,18 @@ func formatNamespaceList(namespaces *corev1.NamespaceList, labelSelector string)
status = "Active"
}

result.WriteString(fmt.Sprintf("• %s: Status=%s, Age=%s",
ns.Name, status, formatDuration(age)))
fmt.Fprintf(&result, "• %s: Status=%s, Age=%s",
ns.Name, status, formatDuration(age))

if len(ns.Labels) > 0 {
labelCount := len(ns.Labels)
result.WriteString(fmt.Sprintf(" - Labels: %d", labelCount))
fmt.Fprintf(&result, " - Labels: %d", labelCount)
}

result.WriteString("\n")
}

result.WriteString(fmt.Sprintf("\nTotal: %d namespace(s)", len(namespaces.Items)))
fmt.Fprintf(&result, "\nTotal: %d namespace(s)", len(namespaces.Items))

return result.String()
}
Expand Down Expand Up @@ -688,21 +688,21 @@ func formatConfigMapList(configMaps *corev1.ConfigMapList, includeNamespace bool
dataCount := len(cm.Data) + len(cm.BinaryData)

if includeNamespace {
result.WriteString(fmt.Sprintf("• %s/%s: Data=%d, Age=%s",
cm.Namespace, cm.Name, dataCount, formatDuration(age)))
fmt.Fprintf(&result, "• %s/%s: Data=%d, Age=%s",
cm.Namespace, cm.Name, dataCount, formatDuration(age))
} else {
result.WriteString(fmt.Sprintf("• %s: Data=%d, Age=%s",
cm.Name, dataCount, formatDuration(age)))
fmt.Fprintf(&result, "• %s: Data=%d, Age=%s",
cm.Name, dataCount, formatDuration(age))
}

if len(cm.Labels) > 0 {
result.WriteString(fmt.Sprintf(" - Labels: %d", len(cm.Labels)))
fmt.Fprintf(&result, " - Labels: %d", len(cm.Labels))
}

result.WriteString("\n")
}

result.WriteString(fmt.Sprintf("\nTotal: %d ConfigMap(s)", len(configMaps.Items)))
fmt.Fprintf(&result, "\nTotal: %d ConfigMap(s)", len(configMaps.Items))

return result.String()
}
Expand Down Expand Up @@ -745,21 +745,21 @@ func formatSecretList(secrets *corev1.SecretList, includeNamespace bool) string
dataCount := len(secret.Data)

if includeNamespace {
result.WriteString(fmt.Sprintf("• %s/%s: Type=%s, Data=%d, Age=%s",
secret.Namespace, secret.Name, secret.Type, dataCount, formatDuration(age)))
fmt.Fprintf(&result, "• %s/%s: Type=%s, Data=%d, Age=%s",
secret.Namespace, secret.Name, secret.Type, dataCount, formatDuration(age))
} else {
result.WriteString(fmt.Sprintf("• %s: Type=%s, Data=%d, Age=%s",
secret.Name, secret.Type, dataCount, formatDuration(age)))
fmt.Fprintf(&result, "• %s: Type=%s, Data=%d, Age=%s",
secret.Name, secret.Type, dataCount, formatDuration(age))
}

if len(secret.Labels) > 0 {
result.WriteString(fmt.Sprintf(" - Labels: %d", len(secret.Labels)))
fmt.Fprintf(&result, " - Labels: %d", len(secret.Labels))
}

result.WriteString("\n")
}

result.WriteString(fmt.Sprintf("\nTotal: %d Secret(s)", len(secrets.Items)))
fmt.Fprintf(&result, "\nTotal: %d Secret(s)", len(secrets.Items))

return result.String()
}
Expand Down Expand Up @@ -812,7 +812,7 @@ func formatJobList(jobs *batchv1.JobList, includeNamespace bool) string {
if includeNamespace {
result.WriteString("Jobs across all namespaces:\n")
} else {
result.WriteString(fmt.Sprintf("Jobs in namespace %q:\n", jobs.Items[0].Namespace))
fmt.Fprintf(&result, "Jobs in namespace %q:\n", jobs.Items[0].Namespace)
}

for _, job := range jobs.Items {
Expand All @@ -832,21 +832,21 @@ func formatJobList(jobs *batchv1.JobList, includeNamespace bool) string {
}

if includeNamespace {
result.WriteString(fmt.Sprintf("• %s/%s: %s - Age: %s",
job.Namespace, job.Name, status, formatDuration(age)))
fmt.Fprintf(&result, "• %s/%s: %s - Age: %s",
job.Namespace, job.Name, status, formatDuration(age))
} else {
result.WriteString(fmt.Sprintf("• %s: %s - Age: %s",
job.Name, status, formatDuration(age)))
fmt.Fprintf(&result, "• %s: %s - Age: %s",
job.Name, status, formatDuration(age))
}

if len(job.Labels) > 0 {
result.WriteString(fmt.Sprintf(" - Labels: %d", len(job.Labels)))
fmt.Fprintf(&result, " - Labels: %d", len(job.Labels))
}

result.WriteString("\n")
}

result.WriteString(fmt.Sprintf("\nTotal: %d Job(s)", len(jobs.Items)))
fmt.Fprintf(&result, "\nTotal: %d Job(s)", len(jobs.Items))

return result.String()
}
Expand Down Expand Up @@ -946,7 +946,7 @@ func formatCronJobList(cronJobs *batchv1.CronJobList, includeNamespace bool) str
result.WriteString("CronJobs across all namespaces:\n")
} else {
if len(cronJobs.Items) > 0 {
result.WriteString(fmt.Sprintf("CronJobs in namespace %q:\n", cronJobs.Items[0].Namespace))
fmt.Fprintf(&result, "CronJobs in namespace %q:\n", cronJobs.Items[0].Namespace)
} else {
result.WriteString("CronJobs in namespace:\n")
}
Expand All @@ -966,21 +966,21 @@ func formatCronJobList(cronJobs *batchv1.CronJobList, includeNamespace bool) str
}

if includeNamespace {
result.WriteString(fmt.Sprintf("• %s/%s: Schedule=%s, %s, LastSchedule=%s, Active=%d, Age=%s",
cronJob.Namespace, cronJob.Name, cronJob.Spec.Schedule, suspended, lastSchedule, len(cronJob.Status.Active), formatDuration(age)))
fmt.Fprintf(&result, "• %s/%s: Schedule=%s, %s, LastSchedule=%s, Active=%d, Age=%s",
cronJob.Namespace, cronJob.Name, cronJob.Spec.Schedule, suspended, lastSchedule, len(cronJob.Status.Active), formatDuration(age))
} else {
result.WriteString(fmt.Sprintf("• %s: Schedule=%s, %s, LastSchedule=%s, Active=%d, Age=%s",
cronJob.Name, cronJob.Spec.Schedule, suspended, lastSchedule, len(cronJob.Status.Active), formatDuration(age)))
fmt.Fprintf(&result, "• %s: Schedule=%s, %s, LastSchedule=%s, Active=%d, Age=%s",
cronJob.Name, cronJob.Spec.Schedule, suspended, lastSchedule, len(cronJob.Status.Active), formatDuration(age))
}

if len(cronJob.Labels) > 0 {
result.WriteString(fmt.Sprintf(" - Labels: %d", len(cronJob.Labels)))
fmt.Fprintf(&result, " - Labels: %d", len(cronJob.Labels))
}

result.WriteString("\n")
}

result.WriteString(fmt.Sprintf("\nTotal: %d CronJob(s)", len(cronJobs.Items)))
fmt.Fprintf(&result, "\nTotal: %d CronJob(s)", len(cronJobs.Items))

return result.String()
}
Expand Down Expand Up @@ -1095,7 +1095,7 @@ func formatIngressList(ingresses *networkingv1.IngressList, includeNamespace boo
result.WriteString("Ingresses across all namespaces:\n")
} else {
if len(ingresses.Items) > 0 {
result.WriteString(fmt.Sprintf("Ingresses in namespace %q:\n", ingresses.Items[0].Namespace))
fmt.Fprintf(&result, "Ingresses in namespace %q:\n", ingresses.Items[0].Namespace)
} else {
result.WriteString("Ingresses in namespace:\n")
}
Expand Down Expand Up @@ -1134,11 +1134,11 @@ func formatIngressList(ingresses *networkingv1.IngressList, includeNamespace boo
}

if includeNamespace {
result.WriteString(fmt.Sprintf("• %s/%s: Class=%s, Hosts=%s, Address=%s, Age=%s",
ingress.Namespace, ingress.Name, ingressClass, hostStr, address, formatDuration(age)))
fmt.Fprintf(&result, "• %s/%s: Class=%s, Hosts=%s, Address=%s, Age=%s",
ingress.Namespace, ingress.Name, ingressClass, hostStr, address, formatDuration(age))
} else {
result.WriteString(fmt.Sprintf("• %s: Class=%s, Hosts=%s, Address=%s, Age=%s",
ingress.Name, ingressClass, hostStr, address, formatDuration(age)))
fmt.Fprintf(&result, "• %s: Class=%s, Hosts=%s, Address=%s, Age=%s",
ingress.Name, ingressClass, hostStr, address, formatDuration(age))
}

// TLS indicator
Expand All @@ -1147,13 +1147,13 @@ func formatIngressList(ingresses *networkingv1.IngressList, includeNamespace boo
}

if len(ingress.Labels) > 0 {
result.WriteString(fmt.Sprintf(" - Labels: %d", len(ingress.Labels)))
fmt.Fprintf(&result, " - Labels: %d", len(ingress.Labels))
}

result.WriteString("\n")
}

result.WriteString(fmt.Sprintf("\nTotal: %d Ingress(es)", len(ingresses.Items)))
fmt.Fprintf(&result, "\nTotal: %d Ingress(es)", len(ingresses.Items))

return result.String()
}
42 changes: 35 additions & 7 deletions cluster/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,43 @@ type Manager struct {
contexts map[string]*kai.ContextInfo
currentContext string
currentNamespace string
requestTimeout time.Duration
}

// New creates a new cluster Manager
func New() *Manager {
return &Manager{
// Option configures a Manager.
type Option func(*Manager)

// WithRequestTimeout sets the per-request timeout applied to every Kubernetes
// API client created by the Manager. Zero or negative values are ignored and
// the 30 s default is kept.
func WithRequestTimeout(d time.Duration) Option {
return func(cm *Manager) {
if d > 0 {
cm.requestTimeout = d
}
}
}

// New creates a new cluster Manager. Without options the default request
// timeout is 30 seconds.
func New(opts ...Option) *Manager {
cm := &Manager{
kubeconfigs: make(map[string]string),
clients: make(map[string]kubernetes.Interface),
dynamicClients: make(map[string]dynamic.Interface),
contexts: make(map[string]*kai.ContextInfo),
currentNamespace: "default",
requestTimeout: 30 * time.Second,
}
for _, opt := range opts {
opt(cm)
}
return cm
}

// RequestTimeout returns the per-request timeout configured on this Manager.
func (cm *Manager) RequestTimeout() time.Duration {
return cm.requestTimeout
}

// LoadKubeConfig loads a kubeconfig file into the manager
Expand All @@ -69,7 +95,7 @@ func (cm *Manager) LoadKubeConfig(name, path string) error {
return err
}

clientset, dynamicClient, err := createClients(resolvedPath)
clientset, dynamicClient, err := cm.createClients(resolvedPath)
if err != nil {
return err
}
Expand Down Expand Up @@ -406,14 +432,16 @@ func (cm *Manager) updateKubeconfigCurrentContext(contextName, configPath string
return nil
}

// createClients creates Kubernetes clients from the kubeconfig file
func createClients(path string) (kubernetes.Interface, dynamic.Interface, error) {
// createClients builds Kubernetes typed and dynamic clients from a kubeconfig
// path. The per-request timeout is taken from the Manager so the user-facing
// --request-timeout flag is honored end-to-end.
func (cm *Manager) createClients(path string) (kubernetes.Interface, dynamic.Interface, error) {
config, err := clientcmd.BuildConfigFromFlags("", path)
if err != nil {
return nil, nil, fmt.Errorf("error building config from flags: %w", err)
}

config.Timeout = 30 * time.Second
config.Timeout = cm.requestTimeout

clientset, err := kubernetes.NewForConfig(config)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/kai/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func main() {
}

// Initialize cluster manager
cm := cluster.New()
cm := cluster.New(cluster.WithRequestTimeout(requestTimeout))

if err := cm.LoadKubeConfig(contextName, kubeconfig); err != nil {
logger.Error("failed to load kubeconfig",
Expand Down
24 changes: 12 additions & 12 deletions tools/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,14 @@ func listContextsHandler(cm kai.ClusterManager) func(ctx context.Context, reques
marker = "*"
}

result.WriteString(fmt.Sprintf("%s %s\n", marker, contextInfo.Name))
result.WriteString(fmt.Sprintf(" Cluster: %s\n", contextInfo.Cluster))
result.WriteString(fmt.Sprintf(" User: %s\n", contextInfo.User))
result.WriteString(fmt.Sprintf(" Namespace: %s\n", contextInfo.Namespace))
fmt.Fprintf(&result, "%s %s\n", marker, contextInfo.Name)
fmt.Fprintf(&result, " Cluster: %s\n", contextInfo.Cluster)
fmt.Fprintf(&result, " User: %s\n", contextInfo.User)
fmt.Fprintf(&result, " Namespace: %s\n", contextInfo.Namespace)
result.WriteString("\n")
}

result.WriteString(fmt.Sprintf("Total: %d context(s)", len(contexts)))
fmt.Fprintf(&result, "Total: %d context(s)", len(contexts))

return mcp.NewToolResultText(result.String()), nil
}
Expand Down Expand Up @@ -265,18 +265,18 @@ func describeContextHandler(cm kai.ClusterManager) func(ctx context.Context, req
}

var result strings.Builder
result.WriteString(fmt.Sprintf("Context: %s\n", contextInfo.Name))
result.WriteString(fmt.Sprintf("Cluster: %s\n", contextInfo.Cluster))
result.WriteString(fmt.Sprintf("User: %s\n", contextInfo.User))
result.WriteString(fmt.Sprintf("Namespace: %s\n", contextInfo.Namespace))
result.WriteString(fmt.Sprintf("Server: %s\n", contextInfo.ServerURL))
result.WriteString(fmt.Sprintf("Config Path: %s\n", contextInfo.ConfigPath))
fmt.Fprintf(&result, "Context: %s\n", contextInfo.Name)
fmt.Fprintf(&result, "Cluster: %s\n", contextInfo.Cluster)
fmt.Fprintf(&result, "User: %s\n", contextInfo.User)
fmt.Fprintf(&result, "Namespace: %s\n", contextInfo.Namespace)
fmt.Fprintf(&result, "Server: %s\n", contextInfo.ServerURL)
fmt.Fprintf(&result, "Config Path: %s\n", contextInfo.ConfigPath)

status := "inactive"
if contextInfo.IsActive {
status = "active"
}
result.WriteString(fmt.Sprintf("Status: %s", status))
fmt.Fprintf(&result, "Status: %s", status)

return mcp.NewToolResultText(result.String()), nil
}
Expand Down
Loading
Loading