package k8sclient import ( "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strings" "time" "gopkg.in/yaml.v3" ) // KubeConfig represents a parsed kubeconfig type KubeConfig struct { APIVersion string `yaml:"apiVersion"` Clusters []struct { Name string `yaml:"name"` Cluster struct { Server string `yaml:"server"` CertificateAuthorityData string `yaml:"certificate-authority-data"` InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify"` } `yaml:"cluster"` } `yaml:"clusters"` Users []struct { Name string `yaml:"name"` User struct { ClientCertificateData string `yaml:"client-certificate-data"` ClientKeyData string `yaml:"client-key-data"` Token string `yaml:"token"` } `yaml:"user"` } `yaml:"users"` Contexts []struct { Name string `yaml:"name"` Context struct { Cluster string `yaml:"cluster"` User string `yaml:"user"` } `yaml:"context"` } `yaml:"contexts"` CurrentContext string `yaml:"current-context"` } // Client wraps an HTTP client for K8s API calls type Client struct { BaseURL string HTTPClient *http.Client } // NewClient creates a K8s API client from kubeconfig YAML func NewClient(kubeconfigYAML string) (*Client, error) { var kcfg KubeConfig if err := yaml.Unmarshal([]byte(kubeconfigYAML), &kcfg); err != nil { return nil, fmt.Errorf("parse kubeconfig: %w", err) } // Find current context ctxName := kcfg.CurrentContext var clusterName, userName string for _, ctx := range kcfg.Contexts { if ctx.Name == ctxName { clusterName = ctx.Context.Cluster userName = ctx.Context.User break } } if clusterName == "" { return nil, fmt.Errorf("current context %q not found", ctxName) } // Find cluster var server string var caData string for _, cl := range kcfg.Clusters { if cl.Name == clusterName { server = cl.Cluster.Server caData = cl.Cluster.CertificateAuthorityData break } } if server == "" { return nil, fmt.Errorf("cluster %q not found", clusterName) } // Build TLS config tlsCfg := &tls.Config{} if caData != "" { caBytes, err := base64.StdEncoding.DecodeString(caData) if err != nil { return nil, fmt.Errorf("decode CA cert: %w", err) } caPool := x509.NewCertPool() caPool.AppendCertsFromPEM(caBytes) tlsCfg.RootCAs = caPool } else { tlsCfg.InsecureSkipVerify = true } // Find user certs for _, u := range kcfg.Users { if u.Name == userName { if u.User.ClientCertificateData != "" && u.User.ClientKeyData != "" { certBytes, _ := base64.StdEncoding.DecodeString(u.User.ClientCertificateData) keyBytes, _ := base64.StdEncoding.DecodeString(u.User.ClientKeyData) cert, err := tls.X509KeyPair(certBytes, keyBytes) if err != nil { return nil, fmt.Errorf("parse client cert: %w", err) } tlsCfg.Certificates = []tls.Certificate{cert} } break } } httpClient := &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ TLSClientConfig: tlsCfg, }, } return &Client{ BaseURL: strings.TrimRight(server, "/"), HTTPClient: httpClient, }, nil } // get performs a GET request to the K8s API func (c *Client) get(path string, result interface{}) error { url := c.BaseURL + path resp, err := c.HTTPClient.Get(url) if err != nil { return fmt.Errorf("GET %s: %w", path, err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("read body: %w", err) } if resp.StatusCode >= 400 { return fmt.Errorf("K8s API error %d: %s", resp.StatusCode, string(body[:min(len(body), 200)])) } if err := json.Unmarshal(body, result); err != nil { return fmt.Errorf("unmarshal: %w (body: %s)", err, string(body[:min(len(body), 100)])) } return nil } // --- K8s API response types --- // NodeList response type NodeList struct { Items []NodeItem `json:"items"` } type NodeItem struct { Metadata ObjectMeta `json:"metadata"` Status NodeStatus `json:"status"` Spec NodeSpec `json:"spec"` } type NodeSpec struct { PodCIDR string `json:"podCIDR"` } type NodeStatus struct { Capacity ResourceList `json:"capacity"` Allocatable ResourceList `json:"allocatable"` Conditions []NodeCondition `json:"conditions"` NodeInfo NodeInfo `json:"nodeInfo"` Addresses []NodeAddress `json:"addresses"` } type ResourceList map[string]string type NodeCondition struct { Type string `json:"type"` Status string `json:"status"` } type NodeInfo struct { KubeletVersion string `json:"kubeletVersion"` OSImage string `json:"osImage"` KernelVersion string `json:"kernelVersion"` ContainerRuntimeVersion string `json:"containerRuntimeVersion"` } type NodeAddress struct { Type string `json:"type"` Address string `json:"address"` } // PodList response type PodList struct { Items []PodItem `json:"items"` } type PodItem struct { Metadata ObjectMeta `json:"metadata"` Status PodStatus `json:"status"` Spec PodSpec `json:"spec"` } type PodSpec struct { NodeName string `json:"nodeName"` Containers []Container `json:"containers"` } type Container struct { Image string `json:"image"` } type PodStatus struct { Phase string `json:"phase"` PodIP string `json:"podIP"` ContainerStatuses []ContainerStatus `json:"containerStatuses"` } type ContainerStatus struct { RestartCount int `json:"restartCount"` Ready bool `json:"ready"` } // DeploymentList response type DeploymentList struct { Items []DeploymentItem `json:"items"` } type DeploymentItem struct { Metadata ObjectMeta `json:"metadata"` Spec DeploymentSpec `json:"spec"` Status DeploymentStatus `json:"status"` } type DeploymentSpec struct { Replicas int `json:"replicas"` Template DeploymentTemplate `json:"template"` } type DeploymentTemplate struct { Spec PodSpec `json:"spec"` } type DeploymentStatus struct { Replicas int `json:"replicas"` AvailableReplicas int `json:"availableReplicas"` ReadyReplicas int `json:"readyReplicas"` } // ServiceList response type ServiceList struct { Items []ServiceItem `json:"items"` } type ServiceItem struct { Metadata ObjectMeta `json:"metadata"` Spec ServiceSpec `json:"spec"` } type ServiceSpec struct { Type string `json:"type"` ClusterIP string `json:"clusterIP"` Ports []ServicePort `json:"ports"` Selector map[string]string `json:"selector"` } type ServicePort struct { Name string `json:"name"` Port int `json:"port"` TargetPort interface{} `json:"targetPort"` NodePort int `json:"nodePort"` Protocol string `json:"protocol"` } // IngressList response type IngressList struct { Items []IngressItem `json:"items"` } type IngressItem struct { Metadata ObjectMeta `json:"metadata"` Spec IngressSpec `json:"spec"` } type IngressSpec struct { TLS []IngressTLS `json:"tls"` Rules []IngressRule `json:"rules"` } type IngressTLS struct { Hosts []string `json:"hosts"` } type IngressRule struct { Host string `json:"host"` } // ObjectMeta type ObjectMeta struct { Name string `json:"name"` Namespace string `json:"namespace"` Labels map[string]string `json:"labels"` Annotations map[string]string `json:"annotations"` CreationTimestamp string `json:"creationTimestamp"` } // SyncResult holds sync counts type SyncResult struct { Nodes int `json:"nodes"` Pods int `json:"pods"` Deployments int `json:"deployments"` Services int `json:"services"` Ingresses int `json:"ingresses"` } // GetServerVersion returns K8s server version func (c *Client) GetServerVersion() (string, error) { var result struct { GitVersion string `json:"gitVersion"` } if err := c.get("/version", &result); err != nil { return "", err } return result.GitVersion, nil } // ListNodes fetches all nodes func (c *Client) ListNodes() (*NodeList, error) { var result NodeList if err := c.get("/api/v1/nodes", &result); err != nil { return nil, err } return &result, nil } // ListPods fetches all pods across namespaces func (c *Client) ListPods() (*PodList, error) { var result PodList if err := c.get("/api/v1/pods", &result); err != nil { return nil, err } return &result, nil } // ListDeployments fetches all deployments across namespaces func (c *Client) ListDeployments() (*DeploymentList, error) { var result DeploymentList if err := c.get("/apis/apps/v1/deployments", &result); err != nil { return nil, err } return &result, nil } // ListServices fetches all services across namespaces func (c *Client) ListServices() (*ServiceList, error) { var result ServiceList if err := c.get("/api/v1/services", &result); err != nil { return nil, err } return &result, nil } // ListIngresses fetches all ingresses across namespaces func (c *Client) ListIngresses() (*IngressList, error) { var result IngressList if err := c.get("/apis/networking.k8s.io/v1/ingresses", &result); err != nil { return nil, err } return &result, nil }