feature/tt/initial-implementation (#1)
Some checks failed
Gitea Docker Build Demo / Test (push) Successful in 2m24s
Gitea Docker Build Demo / Build_Image (push) Failing after 1m40s
Release / Test (release) Successful in 2m22s
Release / Build_Image (release) Failing after 44s

Reviewed-on: #1
Co-authored-by: Tobias Trabelsi <lerentis@uploadfilter24.eu>
Co-committed-by: Tobias Trabelsi <lerentis@uploadfilter24.eu>
This commit is contained in:
2025-10-06 09:21:57 +00:00
committed by lerentis
parent 33921aa992
commit e51594fdd7
17 changed files with 774 additions and 1 deletions

30
internal/config.go Normal file
View File

@@ -0,0 +1,30 @@
package internal
import (
"fmt"
"time"
"github.com/jinzhu/configor"
)
type Config = struct {
LogLevel string `default:"Info" env:"CANADA_KAKTUS_LOGLEVEL"`
LabelSelector string `default:"kops.k8s.io/instance-role=Node" env:"CANADA_KAKTUS_LABELSELECTOR"`
HcloudToken string `default:"" env:"CANADA_KAKTUS_HCLOUD_TOKEN"`
}
func GenConfig() (cfg *Config, err error) {
cfg = &Config{}
err = configor.New(&configor.Config{
ENVPrefix: "CANADA_KAKTUS",
AutoReload: true,
Silent: true,
AutoReloadInterval: time.Minute}).Load(cfg, "config.json")
if err != nil {
return nil, fmt.Errorf("error generating Config: %s", err.Error())
}
return cfg, nil
}

22
internal/config_test.go Normal file
View File

@@ -0,0 +1,22 @@
package internal
import (
"reflect"
"testing"
)
var defaultConfig = Config{
LogLevel: "Info",
LabelSelector: "kops.k8s.io/instance-role=Node",
HcloudToken: "",
}
func TestConfigDefaults(t *testing.T) {
cfg, err := GenConfig()
if err != nil {
t.Errorf("%s", err.Error())
}
if !reflect.DeepEqual(&defaultConfig, cfg) {
t.Errorf("got %+v, want %+v", cfg, defaultConfig)
}
}

31
internal/health.go Normal file
View File

@@ -0,0 +1,31 @@
package internal
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)
func StartHealthEndpoint() {
r := mux.NewRouter()
r.Use(mux.CORSMethodMiddleware(r))
r.HandleFunc("/health", send200).Methods(http.MethodGet)
err := http.ListenAndServe("0.0.0.0:8080", r)
if err != nil {
log.WithFields(log.Fields{
"Caller": "StartHealthEndpoint",
}).Error(fmt.Sprintf("Error creating health endpoint: %s", err.Error()))
}
}
func send200(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte{})
if err != nil {
log.WithFields(log.Fields{
"Caller": "send200",
}).Error(fmt.Sprintf("Error answering health endpoint: %s", err.Error()))
}
}

23
internal/health_test.go Normal file
View File

@@ -0,0 +1,23 @@
package internal
import (
"net/http"
"strings"
"testing"
)
func TestHealth(t *testing.T) {
go func() {
StartHealthEndpoint()
}()
request, _ := http.NewRequest(http.MethodGet, "http://localhost:8080/health", strings.NewReader(""))
resp, err := http.DefaultClient.Do(request)
if err != nil {
t.Errorf("Health endpoint did not start: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Bad response from health endpoint. Want: %d, got %d", http.StatusOK, resp.StatusCode)
}
}

40
internal/hetzner.go Normal file
View File

@@ -0,0 +1,40 @@
package internal
import (
"context"
"fmt"
"github.com/hetznercloud/hcloud-go/hcloud"
log "github.com/sirupsen/logrus"
)
func GetAllNodes(cfg *Config) ([]*hcloud.Server, error) {
client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken))
servers, _, err := client.Server.List(context.TODO(), hcloud.ServerListOpts{
ListOpts: hcloud.ListOpts{
LabelSelector: cfg.LabelSelector,
}})
if err != nil {
return nil, fmt.Errorf("error listing Hetzner Nodes: %s", err.Error())
}
if servers == nil {
return nil, fmt.Errorf("no Nodes found with label selector: %s", cfg.LabelSelector)
}
return servers, nil
}
func GetAllIps(servers []*hcloud.Server) ([]string, error) {
ips := make([]string, len(servers))
for i, instance := range servers {
if instance.PublicNet.IPv4.IP == nil {
return []string{""}, fmt.Errorf("instance %s has no public Addresses", instance.Name)
}
log.WithFields(log.Fields{
"Caller": "GetAllIps",
}).Info(fmt.Sprintf("Found IP: %s", instance.PrivateNet[0].IP.String()))
ips[i] = instance.PublicNet.IPv4.IP.String()
}
return ips, nil
}

30
internal/hetzner_test.go Normal file
View File

@@ -0,0 +1,30 @@
package internal
import (
"testing"
"github.com/hetznercloud/hcloud-go/hcloud"
)
func TestGetAllIps(t *testing.T) {
servers := []*hcloud.Server{{
Status: hcloud.ServerStatusRunning,
Name: "Test",
}}
servers = append(servers, &hcloud.Server{
Status: hcloud.ServerStatusRunning,
Name: "Test2",
})
expectedError := "instance Test has no public Addresses"
_, err := GetAllIps(servers)
if err == nil {
t.Error("GetAllIps did not error with missing data")
}
if err.Error() != expectedError {
t.Errorf("Wrong error message. want %s, got %s", expectedError, err.Error())
}
}

111
internal/k8s.go Normal file
View File

@@ -0,0 +1,111 @@
package internal
import (
"bytes"
"context"
"fmt"
"html/template"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
)
var IP_POOL_TEMPLATE = `
{
"apiVersion": "cilium.io/v2alpha1",
"kind": "CiliumLoadBalancerIPPool",
"metadata": {
"name": "{{ .Name }}",
"annotations": {
"argocd.argoproj.io/tracking-id": "cilium-lb:cilium.io/CiliumLoadBalancerIPPool:kube-system/covidnetes-pool"
}
},
"spec": {
"blocks": [
{{- range $i, $ip := .IPs }}
{{- if $i}},{{ end }}
{
"cidr": "{{ $ip }}"
}
{{- end }}
],
"disabled": false
}
}
`
type CrdConfig struct {
Name string
IPs []string
}
func RecreateIPPoolCrd(cfg *Config, name string, ips []string) error {
routeclient, err := createRestClient()
if err != nil {
return fmt.Errorf("error creating REST Client: %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)
if err != nil {
return fmt.Errorf("could not deserialize CRD: %v", err.Error())
}
res := routeclient.Post().
Resource("routes").
Body(&obj).
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())
}
return nil
}
func createRestClient() (*rest.RESTClient, error) {
k8s_config, err := rest.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("could not create in cluster k8s config: %v", err)
}
k8s_config.APIPath = "/apis"
k8s_config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
routeclient, err := rest.RESTClientFor(k8s_config)
if err != nil {
return nil, fmt.Errorf("could not create k8s client: %v", err)
}
return routeclient, nil
}
func generateIpPool(name string, ips []string) (string, error) {
config := CrdConfig{
Name: name,
IPs: ips,
}
tmpl, err := template.New("ippool").Parse(IP_POOL_TEMPLATE)
if err != nil {
return "", fmt.Errorf("errors in ippool template: %s", err.Error())
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, &config)
if err != nil {
return "", fmt.Errorf("could not render ippool template: %s", err.Error())
}
return buf.String(), nil
}

38
internal/k8s_test.go Normal file
View File

@@ -0,0 +1,38 @@
package internal
import (
"testing"
)
func TestGenerateIpPoolCRD(t *testing.T) {
expected_ip_pool := `
{
"apiVersion": "cilium.io/v2alpha1",
"kind": "CiliumLoadBalancerIPPool",
"metadata": {
"name": "covidnetes-pool",
"annotations": {
"argocd.argoproj.io/tracking-id": "cilium-lb:cilium.io/CiliumLoadBalancerIPPool:kube-system/covidnetes-pool"
}
},
"spec": {
"blocks": [
{
"cidr": "49.13.48.9/32"
},
{
"cidr": "91.107.211.117/32"
}
],
"disabled": false
}
}
`
got, err := generateIpPool("covidnetes-pool", []string{"49.13.48.9/32", "91.107.211.117/32"})
if err != nil {
t.Errorf("%s", err.Error())
}
if expected_ip_pool != got {
t.Errorf("got %+v, want %+v", got, expected_ip_pool)
}
}

28
internal/utils/logging.go Normal file
View File

@@ -0,0 +1,28 @@
package utils
import (
"os"
"git.uploadfilter24.eu/covidnetes/canada-kaktus/internal"
log "github.com/sirupsen/logrus"
)
func ConfigureLogger(cfg *internal.Config) {
switch cfg.LogLevel {
case "Debug":
log.SetLevel(log.DebugLevel)
case "Info":
log.SetLevel(log.InfoLevel)
case "Warn":
log.SetLevel(log.WarnLevel)
case "Error":
log.SetLevel(log.ErrorLevel)
default:
log.SetLevel(log.InfoLevel)
log.Warnf("Home: invalid log level supplied: '%s'", cfg.LogLevel)
}
log.SetFormatter(&log.JSONFormatter{})
log.SetOutput(os.Stdout)
}