some work done

This commit is contained in:
ston1th 2021-03-01 07:40:10 +01:00
commit 09fe1cd163
13 changed files with 174 additions and 45 deletions

View file

@ -1,4 +1,4 @@
package server package api
import ( import (
"context" "context"
@ -10,6 +10,9 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"git.giftfish.de/ston1th/haproxy-lb/pkg/api/types"
serverv1 "git.giftfish.de/ston1th/haproxy-lb/pkg/api/v1/server"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@ -30,6 +33,14 @@ type Server struct {
debug bool debug bool
} }
type notFoundHandler struct {
s *Server
}
func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
types.NewContext(w, r, nf.s).NotFound()
}
// NewHTTPServer returns a new HTTPServer // NewHTTPServer returns a new HTTPServer
func NewServer(log logr.Logger) *Server { func NewServer(log logr.Logger) *Server {
s := &Server{ s := &Server{
@ -46,7 +57,7 @@ func NewServer(log logr.Logger) *Server {
debug: core.C.Debug, debug: core.C.Debug,
} }
s.mux.NotFoundHandler = &notFoundHandler{s} s.mux.NotFoundHandler = &notFoundHandler{s}
for _, v := range routes { for _, v := range serverv1.Routes {
s.mux.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...) s.mux.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
} }
s.start() s.start()
@ -73,9 +84,9 @@ func (s *Server) start() error {
return nil return nil
} }
func (s *Server) contextWrapper(h ctxHandler) http.HandlerFunc { func (s *Server) contextWrapper(h types.CtxHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
h(newContext(w, r, s)) h(types.NewContext(w, r, s))
} }
} }

View file

@ -1,21 +1,22 @@
package server package types
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"github.com/go-logr/logr"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
func newContext(w http.ResponseWriter, r *http.Request, srv *Server) *Context { func NewContext(w http.ResponseWriter, r *http.Request, log logr.Logger) *Context {
h := w.Header() h := w.Header()
h.Set("Content-Type", "application/json") h.Set("Content-Type", "application/json")
return &Context{ return &Context{
Status: http.StatusOK, Status: http.StatusOK,
Request: r, Request: r,
Response: w, Response: w,
s: srv, log: log,
} }
} }
@ -26,7 +27,7 @@ type Context struct {
Request *http.Request Request *http.Request
Response http.ResponseWriter Response http.ResponseWriter
s *Server log logr.Logger
} }
// Method returns the request method // Method returns the request method
@ -57,7 +58,7 @@ func (c *Context) Var(name string) (ret string) {
} }
func (c *Context) log() { func (c *Context) log() {
c.s.log.Info("access", c.log.Info("access",
"addr", c.Request.RemoteAddr, "addr", c.Request.RemoteAddr,
"method", c.Request.Method, "method", c.Request.Method,
"url", c.Request.URL, "url", c.Request.URL,
@ -100,4 +101,4 @@ func (c *Context) HTTPErr(err string, status int) {
c.log() c.log()
} }
type ctxHandler func(*Context) type CtxHandler func(*Context)

View file

@ -1,4 +1,4 @@
package api package types
type Route struct { type Route struct {
Path string Path string

View file

@ -5,13 +5,9 @@ healthCheckNodePort
*/ */
type LBConfig struct {
LoadBalancers []LoadBalancer
}
type LoadBalancer struct { type LoadBalancer struct {
Name string `json:"name"` Name string `json:"name"`
IP string `json:"ip"` IP string `json:"ip,omitempty"`
Options Options `json:"options,omitempty"` Options Options `json:"options,omitempty"`
Ports []Port `json:"ports,omitempty"` Ports []Port `json:"ports,omitempty"`
} }

View file

@ -3,18 +3,12 @@ package server
import ( import (
//"golang.org/x/crypto/bcrypt" //"golang.org/x/crypto/bcrypt"
"net/http" "net/http"
"git.giftfish.de/ston1th/haproxy-lb/pkg/api/types"
) )
type notFoundHandler struct { func authHandler(h types.CtxHandler) types.CtxHandler {
s *Server return func(ctx *types.Context) {
}
func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
newContext(w, r, nf.s).NotFound()
}
func authHandler(h ctxHandler) ctxHandler {
return func(ctx *Context) {
// TODO // TODO
// test:123456 // test:123456
// test:$2b$10$zNfGTAQ94vifj2wtGsv1W.yZRe4/rDBzQixpB6FbrTed4BnHuNqBS // test:$2b$10$zNfGTAQ94vifj2wtGsv1W.yZRe4/rDBzQixpB6FbrTed4BnHuNqBS
@ -23,7 +17,7 @@ func authHandler(h ctxHandler) ctxHandler {
} }
} }
func healthzHandler(ctx *Context) { func healthzHandler(ctx *types.Context) {
// TODO maybe report etcd/raft stats // TODO maybe report etcd/raft stats
ctx.OK() ctx.OK()
} }

View file

@ -1,13 +1,13 @@
package server package server
import ( import (
"git.giftfish.de/ston1th/haproxy-lb/pkg/api" "git.giftfish.de/ston1th/haproxy-lb/pkg/api/types"
schemav1 "git.giftfish.de/ston1th/haproxy-lb/pkg/api/v1/schema" schemav1 "git.giftfish.de/ston1th/haproxy-lb/pkg/api/v1/schema"
) )
const v1 = "/" + schemav1.Version const v1 = "/" + schemav1.Version
var routes = []api.Route{ var routes = []types.Route{
{ {
"/healthz", "/healthz",
healthzHandler, healthzHandler,

View file

@ -16,6 +16,7 @@ type Cluster interface {
type KV interface { type KV interface {
Get(k string) ([]byte, error) Get(k string) ([]byte, error)
GetPrefix(k string) (map[string][]byte, error)
Set(k string, v []byte) error Set(k string, v []byte) error
Delete(k string) error Delete(k string) error
} }

View file

@ -5,17 +5,60 @@ import (
"os/exec" "os/exec"
"time" "time"
"git.giftfish.de/ston1th/vipman/pkg/cluster" "git.giftfish.de/ston1th/haproxy-kb/pkg/cluster"
"git.giftfish.de/ston1th/vipman/pkg/config" "git.giftfish.de/ston1th/haproxy-kb/pkg/config"
"git.giftfish.de/ston1th/vipman/pkg/vip" "git.giftfish.de/ston1th/haproxy-kb/pkg/haproxy"
"git.giftfish.de/ston1th/haproxy-kb/pkg/vip"
"github.com/go-logr/logr" "github.com/go-logr/logr"
) )
func NewVIPController(cfg *config.Config) (callbacks cluster.Callbacks, err error) { type UpdateAction int
const (
Add UpdateAction = iota
Delete
)
type ConfigUpdate struct {
Action UpdateAction
Config haproxy.LoadBalancer
}
func readConfigUpdates(c chan ConfigUpdate) (cu []ConfigUpdate) {
for len(c) > 0 {
select {
case config := <-c:
cu = append(cu, config)
}
}
return
}
func applyConfigUpdates(cc cluster.CallbackContext, cu []ConfigUpdate) error {
for _, c := range cu {
switch c.Action {
case Add:
addIPs(cc, n, []string{c.Config.IP})
cc.Set(c.Config.IP, nil)
case Delete:
cc.Delete(c.Config.IP)
deleteIPs(cc, n, []string{c.Config.IP})
}
}
}
func NewLBController(cfg *config.Config) (callbacks cluster.Callbacks, err error) {
n, err := vip.NewNetworkWithLabel(cfg.Interface, cfg.Label) n, err := vip.NewNetworkWithLabel(cfg.Interface, cfg.Label)
if err != nil { if err != nil {
return return
} }
//TODO
ha, err := haproxy.NewHAProxyManager("")
if err != nil {
return
}
configUpdate := make(chan ConfigUpdate, 100)
callbacks = cluster.Callbacks{ callbacks = cluster.Callbacks{
Leader: func(ctx context.Context, cc cluster.CallbackContext) { Leader: func(ctx context.Context, cc cluster.CallbackContext) {
t := time.NewTicker(time.Second * 10) t := time.NewTicker(time.Second * 10)
@ -29,7 +72,20 @@ func NewVIPController(cfg *config.Config) (callbacks cluster.Callbacks, err erro
} }
for { for {
cc.Info("leading", "id", cc.ID()) cc.Info("leading", "id", cc.ID())
cfg, err := cc.Get("config")
if err != nil {
cc.Error("error getting config key", err)
<-t.C
continue
}
addIPs(cc, n, cfg.VirtualIPs) addIPs(cc, n, cfg.VirtualIPs)
var lbs haproxy.Config
err = ha.UpdateConfig(ctx, lbs)
if err != nil {
cc.Error("error updating haproxy config", err)
<-t.C
continue
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
t.Stop() t.Stop()

View file

@ -3,6 +3,7 @@ package etcd
import ( import (
"context" "context"
"errors" "errors"
"strings"
"git.giftfish.de/ston1th/vipman/pkg/cluster" "git.giftfish.de/ston1th/vipman/pkg/cluster"
//"go.etcd.io/etcd/clientv3" //"go.etcd.io/etcd/clientv3"
@ -52,6 +53,23 @@ func (c *Cluster) Get(k string) (v []byte, err error) {
return return
} }
func (c *Cluster) GetPrefix(k string) (m map[string][]byte, err error) {
r, err := c.cli.Get(c.ctx, c.kvPrefix+"/"+k, clientv3.WithPrefix())
if err != nil {
return
}
if len(r.Kvs) == 0 {
err = ErrKeyNotFound
return
}
m = make(map[string][]byte)
for _, kv := range r.Kvs {
k := strings.TrimPrefix(c.kvPrefix+"/", string(kv.Key))
m[k] = kv.Value
}
return
}
func (c *Cluster) Set(k string, v []byte) (err error) { func (c *Cluster) Set(k string, v []byte) (err error) {
_, err = c.cli.Put(c.ctx, c.kvPrefix+"/"+k, string(v)) _, err = c.cli.Put(c.ctx, c.kvPrefix+"/"+k, string(v))
return return

29
pkg/haproxy/config.go Normal file
View file

@ -0,0 +1,29 @@
package haproxy
type Config struct {
LoadBalancers []LoadBalancer
}
type LoadBalancer struct {
Name string
IP string
Options Options
Ports []Port
}
type Options struct {
Frontend []string
Backend []string
DefaultServer string
}
type Port struct {
Port int
Servers []Server
}
type Server struct {
Name string
IP string
Port int
}

View file

@ -1,7 +1,10 @@
package haproxy package haproxy
import ( import (
"bytes"
"context" "context"
"crypto/sha256"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"sync" "sync"
@ -18,6 +21,7 @@ type HAProxyManager struct {
configFile string configFile string
serviceManager ServiceManager serviceManager ServiceManager
template *template.Template template *template.Template
hash []byte
} }
func NewHAProxyManager(configFile string) (*HAProxyManager, error) { func NewHAProxyManager(configFile string) (*HAProxyManager, error) {
@ -32,26 +36,32 @@ func NewHAProxyManager(configFile string) (*HAProxyManager, error) {
}, nil }, nil
} }
func (ha *HAProxyManager) checkConfig(ctx context.Context, configFile string) error { func (ha *HAProxyManager) checkConfig(ctx context.Context, lbs Config) (hash []byte, err error) {
return exec.CommandContext(ctx, "haproxy", "-c", "-f", configFile).Run()
}
func (ha *HAProxyManager) UpdateConfig(ctx context.Context, lbs []config.LoadBalancer) error {
ha.Lock()
defer ha.Unlock()
tmp, err := ioutil.TempFile(os.TempDir(), "haproxy_") tmp, err := ioutil.TempFile(os.TempDir(), "haproxy_")
if err != nil { if err != nil {
return err return
} }
defer tmp.Close() defer tmp.Close()
defer os.Remove(tmp.Name()) defer os.Remove(tmp.Name())
err = ha.template.Execute(tmp, lbs) h := sha256.New()
w := io.MultiWriter(tmp, h)
err = ha.template.Execute(w, lbs)
if err != nil {
return
}
hash = h.Sum(nil)
return hash, exec.CommandContext(ctx, "haproxy", "-c", "-f", tmp.Name()).Run()
}
func (ha *HAProxyManager) UpdateConfig(ctx context.Context, lbs Config) error {
ha.Lock()
defer ha.Unlock()
hash, err := ha.checkConfig(ctx, lbs)
if err != nil { if err != nil {
return err return err
} }
err = ha.checkConfig(ctx, tmp.Name()) if bytes.Equal(ha.hash, hash) {
if err != nil { return nil
return err
} }
file, err := os.OpenFile(ha.configFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) file, err := os.OpenFile(ha.configFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil { if err != nil {
@ -63,5 +73,6 @@ func (ha *HAProxyManager) UpdateConfig(ctx context.Context, lbs []config.LoadBal
return err return err
} }
file.Close() file.Close()
ha.hash = hash
return ha.serviceManager.Reload() return ha.serviceManager.Reload()
} }

View file

@ -15,7 +15,7 @@ defaults
timeout client 5000ms timeout client 5000ms
timeout tunnel 3600s timeout tunnel 3600s
timeout check 10s timeout check 10s
{{ range $item := . -}} {{ range $item := .LoadBalancers -}}
{{ range $port := $item.Ports -}} {{ range $port := $item.Ports -}}
frontend fr_{{ $item.Name }}_{{ $port.Port }} frontend fr_{{ $item.Name }}_{{ $port.Port }}
bind {{ $item.IP }}:{{ $port.Port }} bind {{ $item.IP }}:{{ $port.Port }}

View file

@ -24,6 +24,18 @@ func (kv *kv) Get(k string) ([]byte, error) {
return kv.m[k], nil return kv.m[k], nil
} }
func (kv *kv) GetPrefix(pk string) (m map[string][]byte, err error) {
kv.mu.Lock()
defer kv.mu.Unlock()
m = make(map[string][]byte)
for k, v := range kv.m {
if strings.HasPrefix(k, pk) {
m[k] = v
}
}
return
}
func (kv *kv) Set(k string, v []byte) error { func (kv *kv) Set(k string, v []byte) error {
if kv.r.State() != raft.Leader { if kv.r.State() != raft.Leader {
return ErrNotLeader return ErrNotLeader