16 Commits

Author SHA1 Message Date
4f81479069 fix(): also drop api server response to debug in case there are no errors
All checks were successful
Gitea Docker Build Demo / Test (push) Successful in 1m0s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m20s
Release / Test (release) Successful in 1m5s
Release / Build_Image (release) Successful in 1m19s
2025-10-07 11:18:39 +02:00
d3682557b1 cleanup and refactor health service
All checks were successful
Gitea Docker Build Demo / Test (push) Successful in 1m2s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m22s
2025-10-07 11:13:15 +02:00
60844be81b fix(): catch errors and increase verbosity
All checks were successful
Gitea Docker Build Demo / Test (push) Successful in 1m0s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m19s
2025-10-07 10:54:45 +02:00
76fb779d08 bloaded put
All checks were successful
Gitea Docker Build Demo / Test (push) Successful in 1m0s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m18s
2025-10-07 10:39:42 +02:00
bafd97fbaf fix(): back to post
All checks were successful
Gitea Docker Build Demo / Test (push) Successful in 1m1s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m26s
2025-10-07 10:22:49 +02:00
49252a5f7a feat(): more verbosity
All checks were successful
Gitea Docker Build Demo / Test (push) Successful in 1m1s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m20s
2025-10-07 10:14:49 +02:00
091ab2eb2f fix(): switch to post
Some checks failed
Gitea Docker Build Demo / Test (push) Failing after 1m2s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m23s
2025-10-07 09:52:58 +02:00
cd1dca0d5e fix(): test !!
Some checks failed
Gitea Docker Build Demo / Test (push) Failing after 1m3s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m25s
2025-10-07 09:49:54 +02:00
b43b31335e fix(): test
Some checks failed
Gitea Docker Build Demo / Test (push) Failing after 1m0s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m18s
2025-10-07 09:46:36 +02:00
6fdb629cc9 fix(): use correct resource path
Some checks failed
Gitea Docker Build Demo / Test (push) Failing after 1m1s
Gitea Docker Build Demo / Build_Image (push) Has been cancelled
2025-10-07 09:44:50 +02:00
5e3ee60c91 fix(): add cidr range to template
Some checks failed
Gitea Docker Build Demo / Build_Image (push) Successful in 1m22s
Gitea Docker Build Demo / Test (push) Failing after 1m5s
2025-10-07 09:33:52 +02:00
eacc8ac9f2 fix(): just use raw json
All checks were successful
Gitea Docker Build Demo / Test (push) Successful in 1m4s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m24s
2025-10-07 09:25:28 +02:00
79fd7ff3b7 fix(): explicitly register cilium schema
All checks were successful
Gitea Docker Build Demo / Test (push) Successful in 1m4s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m23s
2025-10-07 09:17:22 +02:00
6083039648 fix(): explicit object identifier
All checks were successful
Gitea Docker Build Demo / Test (push) Successful in 1m4s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m23s
2025-10-07 08:59:30 +02:00
85a62d24f4 fix(): DOCKERFILES NEED TO SCREAM AT PEOPLE
All checks were successful
Gitea Docker Build Demo / Build_Image (push) Successful in 1m14s
Gitea Docker Build Demo / Test (push) Successful in 1m2s
Release / Test (release) Successful in 1m2s
Release / Build_Image (release) Successful in 1m21s
2025-10-07 08:39:37 +02:00
6ffe638db1 fix(): k8s client call and drop cosign for now
All checks were successful
Gitea Docker Build Demo / Test (push) Successful in 1m6s
Gitea Docker Build Demo / Build_Image (push) Successful in 1m16s
2025-10-07 08:36:21 +02:00
10 changed files with 139 additions and 38 deletions

View File

@@ -40,10 +40,10 @@ jobs:
with:
context: .
file: ./Dockerfile
push: false
push: true
tags: |
lerentis/canada-kaktus:${{ github.sha }}
- name: Sign the published Docker image
env:
COSIGN_EXPERIMENTAL: "true"
run: cosign sign lerentis/canada-kaktus:${{ github.sha }}@${{ steps.build-and-push.outputs.digest }}
# - name: Sign the published Docker image
# env:
# COSIGN_EXPERIMENTAL: "true"
# run: cosign sign lerentis/canada-kaktus:${{ github.sha }}@${{ steps.build-and-push.outputs.digest }}

View File

@@ -40,7 +40,7 @@ jobs:
push: true
tags: |
lerentis/canada-kaktus:${{ github.event.release.tag_name }}
- name: Sign the published Docker image
env:
COSIGN_EXPERIMENTAL: "true"
run: cosign sign lerentis/canada-kaktus:${{ github.event.release.tag_name }}@${{ steps.build-and-push.outputs.digest }}
# - name: Sign the published Docker image
# env:
# COSIGN_EXPERIMENTAL: "true"
# run: cosign sign lerentis/canada-kaktus:${{ github.event.release.tag_name }}@${{ steps.build-and-push.outputs.digest }}

View File

@@ -1,4 +1,4 @@
FROM golang:1.24 as build
FROM golang:1.24 AS build
WORKDIR /app

View File

@@ -2,6 +2,7 @@ package main
import (
"fmt"
"net/http"
"time"
"git.uploadfilter24.eu/covidnetes/canada-kaktus/internal"
@@ -20,11 +21,13 @@ func main() {
}).Fatal(fmt.Sprintf("Error generating Config: %s", err.Error()))
}
hs := internal.NewHealthServer()
go func() {
log.WithFields(log.Fields{
"Caller": "Main",
}).Info("Starting Health Endpoint")
internal.StartHealthEndpoint()
hs.Start()
}()
log.WithFields(log.Fields{
@@ -37,18 +40,21 @@ func main() {
log.WithFields(log.Fields{
"Caller": "Main",
}).Error(fmt.Sprintf("Error getting all Nodes: %s", err.Error()))
hs.SetHealthState(http.StatusServiceUnavailable)
}
ips, err := internal.GetAllIps(servers)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Main",
}).Error(fmt.Sprintf("Error getting all IPs: %s", err.Error()))
hs.SetHealthState(http.StatusServiceUnavailable)
}
err = internal.RecreateIPPoolCrd(cfg, "covidnetes-pool", ips)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Main",
}).Error(fmt.Sprintf("Error recreating IP Pool CRD: %s", err.Error()))
hs.SetHealthState(http.StatusServiceUnavailable)
} else {
log.WithFields(log.Fields{
"Caller": "Main",

2
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/hetznercloud/hcloud-go v1.59.2
github.com/jinzhu/configor v1.2.2
github.com/sirupsen/logrus v1.9.3
k8s.io/apimachinery v0.34.1
k8s.io/client-go v0.34.1
)
@@ -38,7 +39,6 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.1 // indirect
k8s.io/apimachinery v0.34.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect

View File

@@ -3,29 +3,54 @@ package internal
import (
"fmt"
"net/http"
"sync"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)
func StartHealthEndpoint() {
type HealthServer struct {
mu sync.RWMutex
state int
}
func NewHealthServer() *HealthServer {
return &HealthServer{
state: http.StatusOK,
}
}
func (hs *HealthServer) SetHealthState(code int) {
hs.mu.Lock()
defer hs.mu.Unlock()
hs.state = code
}
func (hs *HealthServer) GetHealthState() int {
hs.mu.RLock()
defer hs.mu.RUnlock()
return hs.state
}
func (hs *HealthServer) Start() {
r := mux.NewRouter()
r.Use(mux.CORSMethodMiddleware(r))
r.HandleFunc("/health", send200).Methods(http.MethodGet)
r.HandleFunc("/health", hs.sendHealth).Methods(http.MethodGet)
err := http.ListenAndServe("0.0.0.0:8080", r)
if err != nil {
log.WithFields(log.Fields{
"Caller": "StartHealthEndpoint",
"Caller": "HealthServer.Start",
}).Error(fmt.Sprintf("Error creating health endpoint: %s", err.Error()))
}
}
func send200(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
func (hs *HealthServer) sendHealth(w http.ResponseWriter, r *http.Request) {
code := hs.GetHealthState()
w.WriteHeader(code)
_, err := w.Write([]byte{})
if err != nil {
log.WithFields(log.Fields{
"Caller": "send200",
"Caller": "HealthServer.sendHealth",
}).Error(fmt.Sprintf("Error answering health endpoint: %s", err.Error()))
}
}

View File

@@ -7,8 +7,9 @@ import (
)
func TestHealth(t *testing.T) {
hs := NewHealthServer()
go func() {
StartHealthEndpoint()
hs.Start()
}()
request, _ := http.NewRequest(http.MethodGet, "http://localhost:8080/health", strings.NewReader(""))
resp, err := http.DefaultClient.Do(request)

View File

@@ -18,11 +18,17 @@ func GetAllNodes(cfg *Config) ([]*hcloud.Server, error) {
return nil, fmt.Errorf("error listing Hetzner Nodes: %s", err.Error())
}
if servers == nil {
if len(servers) == 0 {
return nil, fmt.Errorf("no Nodes found with label selector: %s", cfg.LabelSelector)
}
return servers, nil
for _, instance := range servers {
log.WithFields(log.Fields{
"Caller": "GetAllNodes",
}).Debugf("Found server: %s", instance.Name)
}
return servers, nil
}
func GetAllIps(servers []*hcloud.Server) ([]string, error) {
@@ -33,7 +39,7 @@ func GetAllIps(servers []*hcloud.Server) ([]string, error) {
}
log.WithFields(log.Fields{
"Caller": "GetAllIps",
}).Info(fmt.Sprintf("Found IP: %s", instance.PrivateNet[0].IP.String()))
}).Debugf("Found IP: %s", instance.PublicNet.IPv4.IP.String())
ips[i] = instance.PublicNet.IPv4.IP.String()
}
return ips, nil

View File

@@ -3,13 +3,21 @@ package internal
import (
"bytes"
"context"
"encoding/json"
"fmt"
"html/template"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
)
var CILIUM_GROUP_VERSION = schema.GroupVersion{
Group: "cilium.io",
Version: "v2alpha1",
}
var IP_POOL_TEMPLATE = `
{
"apiVersion": "cilium.io/v2alpha1",
@@ -17,7 +25,8 @@ var IP_POOL_TEMPLATE = `
"metadata": {
"name": "{{ .Name }}",
"annotations": {
"argocd.argoproj.io/tracking-id": "cilium-lb:cilium.io/CiliumLoadBalancerIPPool:kube-system/covidnetes-pool"
"argocd.argoproj.io/tracking-id": "cilium-lb:cilium.io/CiliumLoadBalancerIPPool:kube-system/covidnetes-pool",
"managed-by": "canada-kaktus"
}
},
"spec": {
@@ -25,7 +34,7 @@ var IP_POOL_TEMPLATE = `
{{- range $i, $ip := .IPs }}
{{- if $i}},{{ end }}
{
"cidr": "{{ $ip }}"
"cidr": "{{ $ip }}/32"
}
{{- end }}
],
@@ -41,40 +50,68 @@ type CrdConfig struct {
func RecreateIPPoolCrd(cfg *Config, name string, ips []string) error {
routeclient, err := createRestClient()
if len(ips) == 0 {
return fmt.Errorf("no IPs provided to create IP Pool CRD")
}
routeclient, err := createRestClient()
if err != nil {
return fmt.Errorf("error creating REST Client: %v", err.Error())
}
body, err := generateIpPool(name, ips)
resourceVersion, err := getResourceVersion(routeclient, name)
if err != nil {
return fmt.Errorf("error getting resourceVersion: %v", err.Error())
}
body, err := generateIpPool(name, ips)
if err != nil {
return fmt.Errorf("error generating CRD: %v", err.Error())
}
decode := scheme.Codecs.UniversalDeserializer().Decode
obj, _, err := decode([]byte(body), nil, nil)
// Inject resourceVersion into the JSON
var obj map[string]interface{}
if err := json.Unmarshal([]byte(body), &obj); err != nil {
return fmt.Errorf("could not unmarshal generated CRD: %v", err)
}
if meta, ok := obj["metadata"].(map[string]interface{}); ok {
meta["resourceVersion"] = resourceVersion
}
finalBody, err := json.Marshal(obj)
if err != nil {
return fmt.Errorf("could not deserialize CRD: %v", err.Error())
return fmt.Errorf("could not marshal final CRD: %v", err)
}
res := routeclient.Post().
Resource("routes").
Body(&obj).
res := routeclient.Put().
Resource("ciliumloadbalancerippools").
Name(name).
Body(finalBody).
Do(context.TODO())
var status int
res.StatusCode(&status)
if status >= 200 && status <= 400 {
return fmt.Errorf("failed to post CRD to kube api: %v", res.Error().Error())
raw, rawErr := res.Raw()
if status < 200 || status >= 400 {
log.WithFields(log.Fields{
"Caller": "RecreateIPPoolCrd",
}).Warnf("Response from k8s api server: %s", string(raw))
return fmt.Errorf("failed to post CRD to kube api: %v", res.Error())
}
log.WithFields(log.Fields{
"Caller": "RecreateIPPoolCrd",
}).Debugf("Response from k8s api server: %s", string(raw))
if rawErr != nil {
log.WithFields(log.Fields{
"Caller": "RecreateIPPoolCrd",
}).Warnf("Could not get raw response from k8s api server: %v", rawErr)
}
return nil
}
func createRestClient() (*rest.RESTClient, error) {
k8s_config, err := rest.InClusterConfig()
if err != nil {
@@ -83,6 +120,7 @@ func createRestClient() (*rest.RESTClient, error) {
k8s_config.APIPath = "/apis"
k8s_config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
k8s_config.GroupVersion = &CILIUM_GROUP_VERSION
routeclient, err := rest.RESTClientFor(k8s_config)
@@ -109,3 +147,27 @@ func generateIpPool(name string, ips []string) (string, error) {
}
return buf.String(), nil
}
func getResourceVersion(client *rest.RESTClient, name string) (string, error) {
res := client.Get().
Resource("ciliumloadbalancerippools").
Name(name).
Do(context.TODO())
raw, err := res.Raw()
if err != nil {
return "", fmt.Errorf("could not fetch CRD: %v", err)
}
var obj map[string]interface{}
if err := json.Unmarshal(raw, &obj); err != nil {
return "", fmt.Errorf("could not unmarshal CRD: %v", err)
}
meta, ok := obj["metadata"].(map[string]interface{})
if !ok {
return "", fmt.Errorf("metadata missing in CRD")
}
rv, ok := meta["resourceVersion"].(string)
if !ok {
return "", fmt.Errorf("resourceVersion missing in metadata")
}
return rv, nil
}

View File

@@ -12,7 +12,8 @@ func TestGenerateIpPoolCRD(t *testing.T) {
"metadata": {
"name": "covidnetes-pool",
"annotations": {
"argocd.argoproj.io/tracking-id": "cilium-lb:cilium.io/CiliumLoadBalancerIPPool:kube-system/covidnetes-pool"
"argocd.argoproj.io/tracking-id": "cilium-lb:cilium.io/CiliumLoadBalancerIPPool:kube-system/covidnetes-pool",
"managed-by": "canada-kaktus"
}
},
"spec": {
@@ -28,7 +29,7 @@ func TestGenerateIpPoolCRD(t *testing.T) {
}
}
`
got, err := generateIpPool("covidnetes-pool", []string{"49.13.48.9/32", "91.107.211.117/32"})
got, err := generateIpPool("covidnetes-pool", []string{"49.13.48.9", "91.107.211.117"})
if err != nil {
t.Errorf("%s", err.Error())
}