Service config. Service logs. Service status.

This commit is contained in:
2025-10-14 05:26:45 +00:00
parent 1d11996cd6
commit afb0e09aae
9 changed files with 1316 additions and 11 deletions

View File

@@ -1,7 +1,11 @@
package tools
import (
"encoding/json"
"fmt"
"os/exec"
"strings"
"time"
)
// Kubectl provides a thin wrapper around the kubectl command-line tool
@@ -31,3 +35,251 @@ func (k *Kubectl) DeploymentExists(name, namespace string) bool {
err := cmd.Run()
return err == nil
}
// PodInfo represents pod information from kubectl
type PodInfo struct {
Name string
Status string
Ready string
Restarts int
Age string
Node string
IP string
}
// DeploymentInfo represents deployment information
type DeploymentInfo struct {
Desired int32
Current int32
Ready int32
Available int32
}
// GetPods retrieves pod information for a namespace
func (k *Kubectl) GetPods(namespace string) ([]PodInfo, error) {
args := []string{
"get", "pods",
"-n", namespace,
"-o", "json",
}
if k.kubeconfigPath != "" {
args = append([]string{"--kubeconfig", k.kubeconfigPath}, args...)
}
cmd := exec.Command("kubectl", args...)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to get pods: %w", err)
}
var podList struct {
Items []struct {
Metadata struct {
Name string `json:"name"`
CreationTimestamp time.Time `json:"creationTimestamp"`
} `json:"metadata"`
Spec struct {
NodeName string `json:"nodeName"`
} `json:"spec"`
Status struct {
Phase string `json:"phase"`
PodIP string `json:"podIP"`
ContainerStatuses []struct {
Ready bool `json:"ready"`
RestartCount int `json:"restartCount"`
} `json:"containerStatuses"`
} `json:"status"`
} `json:"items"`
}
if err := json.Unmarshal(output, &podList); err != nil {
return nil, fmt.Errorf("failed to parse pod list: %w", err)
}
pods := make([]PodInfo, 0, len(podList.Items))
for _, pod := range podList.Items {
// Calculate ready containers
readyCount := 0
totalCount := len(pod.Status.ContainerStatuses)
restarts := 0
for _, cs := range pod.Status.ContainerStatuses {
if cs.Ready {
readyCount++
}
restarts += cs.RestartCount
}
// Calculate age
age := formatAge(time.Since(pod.Metadata.CreationTimestamp))
pods = append(pods, PodInfo{
Name: pod.Metadata.Name,
Status: pod.Status.Phase,
Ready: fmt.Sprintf("%d/%d", readyCount, totalCount),
Restarts: restarts,
Age: age,
Node: pod.Spec.NodeName,
IP: pod.Status.PodIP,
})
}
return pods, nil
}
// GetDeployment retrieves deployment information
func (k *Kubectl) GetDeployment(name, namespace string) (*DeploymentInfo, error) {
args := []string{
"get", "deployment", name,
"-n", namespace,
"-o", "json",
}
if k.kubeconfigPath != "" {
args = append([]string{"--kubeconfig", k.kubeconfigPath}, args...)
}
cmd := exec.Command("kubectl", args...)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to get deployment: %w", err)
}
var deployment struct {
Status struct {
Replicas int32 `json:"replicas"`
UpdatedReplicas int32 `json:"updatedReplicas"`
ReadyReplicas int32 `json:"readyReplicas"`
AvailableReplicas int32 `json:"availableReplicas"`
} `json:"status"`
Spec struct {
Replicas int32 `json:"replicas"`
} `json:"spec"`
}
if err := json.Unmarshal(output, &deployment); err != nil {
return nil, fmt.Errorf("failed to parse deployment: %w", err)
}
return &DeploymentInfo{
Desired: deployment.Spec.Replicas,
Current: deployment.Status.Replicas,
Ready: deployment.Status.ReadyReplicas,
Available: deployment.Status.AvailableReplicas,
}, nil
}
// GetLogs retrieves logs from a pod
func (k *Kubectl) GetLogs(namespace, podName string, opts LogOptions) (string, error) {
args := []string{
"logs", podName,
"-n", namespace,
}
if opts.Container != "" {
args = append(args, "-c", opts.Container)
}
if opts.Tail > 0 {
args = append(args, "--tail", fmt.Sprintf("%d", opts.Tail))
}
if opts.Previous {
args = append(args, "--previous")
}
if opts.Since != "" {
args = append(args, "--since", opts.Since)
}
if k.kubeconfigPath != "" {
args = append([]string{"--kubeconfig", k.kubeconfigPath}, args...)
}
cmd := exec.Command("kubectl", args...)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get logs: %w", err)
}
return string(output), nil
}
// LogOptions configures log retrieval
type LogOptions struct {
Container string
Tail int
Previous bool
Since string
}
// StreamLogs streams logs from a pod
func (k *Kubectl) StreamLogs(namespace, podName string, opts LogOptions) (*exec.Cmd, error) {
args := []string{
"logs", podName,
"-n", namespace,
"-f", // follow
}
if opts.Container != "" {
args = append(args, "-c", opts.Container)
}
if opts.Tail > 0 {
args = append(args, "--tail", fmt.Sprintf("%d", opts.Tail))
}
if opts.Since != "" {
args = append(args, "--since", opts.Since)
}
if k.kubeconfigPath != "" {
args = append([]string{"--kubeconfig", k.kubeconfigPath}, args...)
}
cmd := exec.Command("kubectl", args...)
return cmd, nil
}
// formatAge converts a duration to a human-readable age string
func formatAge(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%ds", int(d.Seconds()))
}
if d < time.Hour {
return fmt.Sprintf("%dm", int(d.Minutes()))
}
if d < 24*time.Hour {
return fmt.Sprintf("%dh", int(d.Hours()))
}
return fmt.Sprintf("%dd", int(d.Hours()/24))
}
// GetFirstPodName returns the name of the first pod in a namespace
func (k *Kubectl) GetFirstPodName(namespace string) (string, error) {
pods, err := k.GetPods(namespace)
if err != nil {
return "", err
}
if len(pods) == 0 {
return "", fmt.Errorf("no pods found in namespace %s", namespace)
}
return pods[0].Name, nil
}
// GetPodContainers returns container names for a pod
func (k *Kubectl) GetPodContainers(namespace, podName string) ([]string, error) {
args := []string{
"get", "pod", podName,
"-n", namespace,
"-o", "jsonpath={.spec.containers[*].name}",
}
if k.kubeconfigPath != "" {
args = append([]string{"--kubeconfig", k.kubeconfigPath}, args...)
}
cmd := exec.Command("kubectl", args...)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to get pod containers: %w", err)
}
containerNames := strings.Fields(string(output))
return containerNames, nil
}