From 3f605bb156e8cd195dba4f8f48ff62ac9f854995 Mon Sep 17 00:00:00 2001 From: basebandit Date: Tue, 12 May 2026 11:52:30 +0300 Subject: [PATCH 1/2] feat(cluster): honor --request-timeout flag in K8s client config --- cluster/manager.go | 42 +++++++++++++++++++++++++++++++++++------- cmd/kai/main.go | 2 +- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/cluster/manager.go b/cluster/manager.go index df417b1..7716d5e 100644 --- a/cluster/manager.go +++ b/cluster/manager.go @@ -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 @@ -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 } @@ -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 { diff --git a/cmd/kai/main.go b/cmd/kai/main.go index 6553492..15dbc87 100644 --- a/cmd/kai/main.go +++ b/cmd/kai/main.go @@ -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", From 434be25c08d66e62c49323b9a149d58c1aa1f3de Mon Sep 17 00:00:00 2001 From: basebandit Date: Tue, 12 May 2026 12:18:50 +0300 Subject: [PATCH 2/2] fix(lint): replace WriteString(Sprintf) with Fprintf (staticcheck QF1012) --- cluster/formatted.go | 86 ++++++++++++++++++++++---------------------- tools/context.go | 24 ++++++------- tools/operations.go | 21 ++++++----- 3 files changed, 65 insertions(+), 66 deletions(-) diff --git a/cluster/formatted.go b/cluster/formatted.go index bf74b9a..e3f6198 100644 --- a/cluster/formatted.go +++ b/cluster/formatted.go @@ -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() } @@ -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") } @@ -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() } @@ -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() } @@ -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() } @@ -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 { @@ -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() } @@ -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") } @@ -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() } @@ -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") } @@ -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 @@ -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() } diff --git a/tools/context.go b/tools/context.go index 1b42b4b..9967286 100644 --- a/tools/context.go +++ b/tools/context.go @@ -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 } @@ -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 } diff --git a/tools/operations.go b/tools/operations.go index 2b9971e..513ba4f 100644 --- a/tools/operations.go +++ b/tools/operations.go @@ -200,15 +200,15 @@ func formatPortForwardSession(session *cluster.PortForwardSession) string { var sb strings.Builder sb.WriteString("Port forward started successfully\n") sb.WriteString(strings.Repeat("-", 40) + "\n") - sb.WriteString(fmt.Sprintf("Session ID: %s\n", session.ID)) - sb.WriteString(fmt.Sprintf("Namespace: %s\n", session.Namespace)) - sb.WriteString(fmt.Sprintf("Target: %s/%s\n", session.TargetType, session.Target)) + fmt.Fprintf(&sb, "Session ID: %s\n", session.ID) + fmt.Fprintf(&sb, "Namespace: %s\n", session.Namespace) + fmt.Fprintf(&sb, "Target: %s/%s\n", session.TargetType, session.Target) if session.TargetType == "service" { - sb.WriteString(fmt.Sprintf("Pod: %s\n", session.PodName)) + fmt.Fprintf(&sb, "Pod: %s\n", session.PodName) } - sb.WriteString(fmt.Sprintf("Forwarding: localhost:%d -> %d\n", session.LocalPort, session.RemotePort)) + fmt.Fprintf(&sb, "Forwarding: localhost:%d -> %d\n", session.LocalPort, session.RemotePort) sb.WriteString(strings.Repeat("-", 40) + "\n") - sb.WriteString(fmt.Sprintf("Access via: http://localhost:%d\n", session.LocalPort)) + fmt.Fprintf(&sb, "Access via: http://localhost:%d\n", session.LocalPort) return sb.String() } @@ -221,8 +221,8 @@ func formatPortForwardList(sessions []*cluster.PortForwardSession) string { var sb strings.Builder sb.WriteString("Active Port Forwards:\n") sb.WriteString(strings.Repeat("-", 80) + "\n") - sb.WriteString(fmt.Sprintf("%-10s %-15s %-25s %-15s %s\n", - "ID", "NAMESPACE", "TARGET", "POD", "PORTS")) + fmt.Fprintf(&sb, "%-10s %-15s %-25s %-15s %s\n", + "ID", "NAMESPACE", "TARGET", "POD", "PORTS") sb.WriteString(strings.Repeat("-", 80) + "\n") for _, session := range sessions { @@ -234,14 +234,13 @@ func formatPortForwardList(sessions []*cluster.PortForwardSession) string { if len(targetDisplay) > 25 { targetDisplay = targetDisplay[:22] + "..." } - sb.WriteString(fmt.Sprintf("%-10s %-15s %-25s %-15s %d:%d\n", + fmt.Fprintf(&sb, "%-10s %-15s %-25s %-15s %d:%d\n", session.ID, session.Namespace, targetDisplay, podDisplay, session.LocalPort, - session.RemotePort, - )) + session.RemotePort) } return sb.String()