package main import ( "crypto/sha1" "crypto/tls" "encoding/hex" "encoding/json" "fmt" "html/template" "io" "log" "net/http" "os" "path/filepath" "strconv" "strings" ) type K8sRawList struct { Items []json.RawMessage `json:"items"` } type BackendServer struct { Name string Address string Port int Cookie string } type Backend struct { Name string Balance string Servers []BackendServer } func getEnv(key, fallback string) string { if val, ok := os.LookupEnv(key); ok && val != "" { return val } return fallback } func hashString(input string) string { h := sha1.New() h.Write([]byte(input)) return hex.EncodeToString(h.Sum(nil))[:8] } func getTemplate(path string) (*template.Template, error) { content, err := os.ReadFile(path) if err != nil { return nil, err } return template.New("haproxy").Parse(string(content)) } func getK8sResources(k8sHost, token string, verifySSL bool, resource string) ([]json.RawMessage, error) { url := fmt.Sprintf("%s/api/v1/%s", k8sHost, resource) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "gateway-config") client := &http.Client{} if !verifySSL { tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } client.Transport = tr } resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) } var list K8sRawList if err := json.NewDecoder(resp.Body).Decode(&list); err != nil { return nil, err } return list.Items, nil } func main() { k8sHost := getEnv("KUBERNETES_HOST", "") k8sToken := getEnv("KUBERNETES_TOKEN", "") verifySSLStr := getEnv("KUBERNETES_VERIFYSSL", "false") templatePath := getEnv("HAPROXY_TEMPLATE", "haproxy.tmpl") if k8sHost == "" || k8sToken == "" { log.Fatal("KUBERNETES_HOST and KUBERNETES_TOKEN must be set") } verifySSL, err := strconv.ParseBool(verifySSLStr) if err != nil { log.Fatalf("Invalid KUBERNETES_VERIFYSSL value: %v", err) } servicesRaw, err := getK8sResources(k8sHost, k8sToken, verifySSL, "services") if err != nil { log.Fatalf("Failed to get services: %v", err) } endpointsRaw, err := getK8sResources(k8sHost, k8sToken, verifySSL, "endpoints") if err != nil { log.Fatalf("Failed to get endpoints: %v", err) } type Service struct { Metadata struct { Name string `json:"name"` Namespace string `json:"namespace"` } `json:"metadata"` Spec struct { Type string `json:"type"` Ports []struct { Port int `json:"port"` } `json:"ports"` } `json:"spec"` } type EndpointSubsetAddress struct { IP string `json:"ip"` } type EndpointSubsetPort struct { Port int `json:"port"` } type EndpointSubset struct { Addresses []EndpointSubsetAddress `json:"addresses"` Ports []EndpointSubsetPort `json:"ports"` } type Endpoint struct { Metadata struct { Name string `json:"name"` Namespace string `json:"namespace"` } `json:"metadata"` Subsets []EndpointSubset `json:"subsets"` } services := make([]Service, 0, len(servicesRaw)) for _, raw := range servicesRaw { var svc Service if err := json.Unmarshal(raw, &svc); err != nil { log.Printf("Warn: failed to unmarshal service: %v", err) continue } services = append(services, svc) } endpoints := make([]Endpoint, 0, len(endpointsRaw)) for _, raw := range endpointsRaw { var ep Endpoint if err := json.Unmarshal(raw, &ep); err != nil { log.Printf("Warn: failed to unmarshal endpoint: %v", err) continue } endpoints = append(endpoints, ep) } endpointMap := make(map[string]Endpoint) for _, ep := range endpoints { key := ep.Metadata.Namespace + "/" + ep.Metadata.Name endpointMap[key] = ep } backends := []Backend{} for _, svc := range services { if svc.Spec.Type != "LoadBalancer" { continue } key := svc.Metadata.Namespace + "/" + svc.Metadata.Name ep, found := endpointMap[key] if !found || len(ep.Subsets) == 0 { continue } b := Backend{ Name: "SRV_" + strings.ReplaceAll(svc.Metadata.Name, " ", "-"), Balance: "leastconn", } servers := []BackendServer{} serverIndex := 1 for _, subset := range ep.Subsets { for _, addr := range subset.Addresses { for _, port := range subset.Ports { cookie := hashString(fmt.Sprintf("%s-%s-%d", svc.Metadata.Name, addr.IP, port.Port)) serverName := fmt.Sprintf("%s_%d", svc.Metadata.Name, serverIndex) serverIndex++ servers = append(servers, BackendServer{ Name: serverName, Address: addr.IP, Port: port.Port, Cookie: cookie, }) } } } b.Servers = servers backends = append(backends, b) } tmplAbsPath, err := filepath.Abs(templatePath) if err != nil { log.Fatalf("Failed to get absolute path: %v", err) } tmpl, err := getTemplate(tmplAbsPath) if err != nil { log.Fatalf("Failed to parse template: %v", err) } err = tmpl.Execute(os.Stdout, map[string]interface{}{ "backends": backends, }) if err != nil { log.Fatalf("Failed to execute template: %v", err) } }