Skip to content
This repository was archived by the owner on Jun 11, 2025. It is now read-only.
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
2 changes: 2 additions & 0 deletions apps/kube-health/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.secrets
bin
5 changes: 5 additions & 0 deletions apps/kube-health/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# syntax=docker/dockerfile:1
FROM gcr.io/distroless/static-debian11:nonroot
ARG BINARY TARGETARCH
COPY --chown=1001 $BINARY-$TARGETARCH /kube-svc-health
ENTRYPOINT ["/kube-svc-health"]
35 changes: 35 additions & 0 deletions apps/kube-health/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: 3

vars:
app: kube-svc-health
binary: "./bin/{{.app}}"

tasks:
run:
dotenv:
- .secrets/env
cmds:
- go run ./main.go

build:
env:
output: "{{.binary}}-{{.GOARCH}}"
GOARCH: "{{.GOARCH}}"
CGO_ENABLED: 0
cmds:
- go build -o $output ./main.go

container:build-and-push:
requires:
vars:
- image
cmds:
- task: build
vars:
GOARCH: amd64

- task: build
vars:
GOARCH: arm64

- docker buildx build --platform linux/amd64,linux/arm64 --build-arg BINARY={{.binary}} -t {{.image}} . --push
161 changes: 161 additions & 0 deletions apps/kube-health/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package main

import (
"context"
"fmt"
"log"

"github.com/codingconcepts/env"
"github.com/gofiber/fiber/v2"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

type Env struct {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Validate KubernetesApiProxy value

Consider adding validation for the KubernetesApiProxy value, especially since it's used directly in the rest.Config. This can help prevent security issues and improve reliability.

type Env struct {
	HttpPort           uint16 `env:"HTTP_PORT" default:"3000"`
	KubernetesApiProxy string `env:"KUBERNETES_API_PROXY" validate:"required,url"`

HttpPort uint16 `env:"HTTP_PORT" default:"3000"`
KubernetesApiProxy string `env:"KUBERNETES_API_PROXY"`
}

func LoadEnv() (*Env, error) {
var e Env
if err := env.Set(&e); err != nil {
return nil, err
}
return &e, nil
}

func main() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider more graceful error handling in main()

Instead of using panic(), consider logging the error and performing a controlled shutdown. This approach is more suitable for a production Kubernetes service.

func main() {
	if err := Run(); err != nil {
		log.Printf("Error running application: %v", err)
		os.Exit(1)
	}

if err := Run(); err != nil {
panic(err)
}
}

func Run() error {
env, err := LoadEnv()
if err != nil {
return err
}

kubeconfig := &rest.Config{
Host: env.KubernetesApiProxy,
}

if env.KubernetesApiProxy == "" {
var err error
kubeconfig, err = rest.InClusterConfig()
if err != nil {
return err
}
}

// Create the Kubernetes client
clientset, err := kubernetes.NewForConfig(kubeconfig)
if err != nil {
return err
}

app := fiber.New()

app.Get("/_healthy", func(c *fiber.Ctx) error {
return c.Status(200).JSON(fiber.Map{
"status": "healthy",
})
})

app.Get("/kubernetes", func(c *fiber.Ctx) error {
b, err := clientset.Discovery().RESTClient().Get().AbsPath("/healthz").DoRaw(context.TODO())
if err != nil {
return err
}

return c.Status(200).Send(b)
})

app.Get("/:ns/:svc", func(c *fiber.Ctx) error {
ns := c.Params("ns")
svc := c.Params("svc")

pods, unhealthy, err := checkSvcHealth(clientset, ns, svc)
if err != nil {
return err
}

healthy := pods - unhealthy

if healthy == 0 {
return c.Status(500).JSON(fiber.Map{
"status": "unhealthy",
"running": fmt.Sprintf("%d/%d", healthy, pods),
})
}

return c.Status(200).JSON(fiber.Map{
"status": "healthy",
"running": fmt.Sprintf("%d/%d", healthy, pods),
})
})

app.All("/*", func(c *fiber.Ctx) error {
return c.Status(404).JSON(fiber.Map{
"status": "not found",
})
})

if err := app.Listen(fmt.Sprintf(":%d", env.HttpPort)); err != nil {
return err
}

return nil
}

func checkSvcHealth(clientset *kubernetes.Clientset, ns, svcName string) (int, int, error) {

if svcName == "" {
return 0, 0, fmt.Errorf("Service name must be provided")
}
if ns == "" {
return 0, 0, fmt.Errorf("Namespace must be provided")
}

// Get the service
svc, err := clientset.CoreV1().Services(ns).Get(context.TODO(), svcName, metav1.GetOptions{})
if err != nil {
return 0, 0, err
}

// List the pods using the service selector
labelSelector := metav1.FormatLabelSelector(&metav1.LabelSelector{MatchLabels: svc.Spec.Selector})
pods, err := clientset.CoreV1().Pods(ns).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector})
if err != nil {
return 0, 0, err
}

count := 0
// Check the status of each pod
for _, pod := range pods.Items {
if err := checkPodHealth(&pod); err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Avoid logging errors within checkSvcHealth

Consider returning errors from this function instead of logging them here. This allows for better separation of concerns and more flexible error handling at higher levels of the application.

		err := checkPodHealth(&pod)
		if err != nil {
			count++
			return fmt.Errorf("pod health check failed: %w", err)
		}

log.Printf("Error: %v", err)
count++
}
}

return len(pods.Items), count, nil
}

func checkPodHealth(pod *v1.Pod) error {
ready := false
for _, condition := range pod.Status.Conditions {
if condition.Type == v1.PodReady && condition.Status == v1.ConditionTrue {
ready = true
break
}
}

if !ready {
return fmt.Errorf("Pod %s is not healthy", pod.Name)
}

return nil
}