implemented api handlers

This commit is contained in:
ston1th 2021-04-08 00:34:54 +02:00
commit 372915e24d
15 changed files with 302 additions and 73 deletions

View file

@ -78,8 +78,8 @@ func main() {
}() }()
sigs := make(chan os.Signal) sigs := make(chan os.Signal)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs s := <-sigs
log.Info("haproxy-lb shutdown") log.Info("haproxy-lb shutdown", "signal", s.String())
c.Stop() c.Stop()
log.Info("haproxy-lb stopped") log.Info("haproxy-lb stopped")
} }

View file

@ -36,22 +36,26 @@ func (a *Alloc) UpdateDB(db *db.DB) {
a.Unlock() a.Unlock()
} }
func (a *Alloc) cidr(ip net.IP) string {
gw := new(net.IPNet)
*gw = *a.gateway
gw.IP = ip
return gw.String()
}
func (a *Alloc) AllocIP(ctx context.Context, name string) (addr string, err error) { func (a *Alloc) AllocIP(ctx context.Context, name string) (addr string, err error) {
a.Lock() a.Lock()
defer a.Unlock() defer a.Unlock()
for _, cidr := range a.pool { for _, cidr := range a.pool {
c := ipaddr.NewCursor([]ipaddr.Prefix{*ipaddr.NewPrefix(cidr)}) c := ipaddr.NewCursor([]ipaddr.Prefix{*ipaddr.NewPrefix(cidr)})
for pos := c.First(); pos != nil; pos = c.Next() { for pos := c.First(); pos != nil; pos = c.Next() {
ip := pos.IP.String() cidr := a.cidr(pos.IP)
if !a.db.IPExists(ip) { if !a.db.IPExists(cidr) {
err = a.db.SetIP(ip, name) err = a.db.SetIP(cidr, name)
if err != nil { if err != nil {
return return
} }
var gw *net.IPNet addr = pos.IP.String()
*gw = *a.gateway
gw.IP = pos.IP
addr = gw.String()
return return
} }
} }

View file

@ -2,6 +2,7 @@ package types
import ( import (
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"github.com/go-logr/logr" "github.com/go-logr/logr"
@ -26,20 +27,19 @@ func NewContext(w http.ResponseWriter, r *http.Request, data *ContextData, log l
Request: r, Request: r,
Response: w, Response: w,
Data: data, Data: data,
logger: log, Log: log,
} }
} }
// Context is the context object shared between http handlers // Context is the context object shared between http handlers
type Context struct { type Context struct {
Body []byte
Status int Status int
Request *http.Request Request *http.Request
Response http.ResponseWriter Response http.ResponseWriter
Data *ContextData Data *ContextData
logger logr.Logger Log logr.Logger
} }
// Method returns the request method // Method returns the request method
@ -70,7 +70,7 @@ func (c *Context) Var(name string) (ret string) {
} }
func (c *Context) log() { func (c *Context) log() {
c.logger.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.Path, "url", c.Request.URL.Path,
@ -79,23 +79,38 @@ func (c *Context) log() {
} }
func (c *Context) NotFound() { func (c *Context) NotFound() {
c.Status = http.StatusNotFound c.Err(ErrNotFound)
c.Response.WriteHeader(http.StatusNotFound)
c.Response.Write([]byte(`{"message":"not found","status":404}`))
c.log()
} }
//TODO func (c *Context) MethodNotAllowed() {
// c.Status = http.StatusMethodNotAllowed
// c.Response.WriteHeader(http.StatusMethodNotAllowed)
// c.Response.Write([]byte(`{"message":"method not allowed","status":405}`))
// c.log()
//}
// OK is a empty HTTP 200 JSON response // OK is a empty HTTP 200 JSON response
func (c *Context) OK() { func (c *Context) OK() {
c.Response.Write([]byte("{}")) c.Response.Write([]byte("{}"))
c.log() c.log()
} }
func (c *Context) ReadBody() (b []byte, err error) {
b, err = io.ReadAll(c.Request.Body)
c.Request.Body.Close()
return
}
func (c *Context) Body(b []byte) {
c.Response.Write(b)
c.log()
}
// JSON is a json response // JSON is a json response
func (c *Context) JSON(v interface{}) { func (c *Context) JSON(v interface{}) {
err := json.NewEncoder(c.Response).Encode(v) err := json.NewEncoder(c.Response).Encode(v)
if err != nil { if err != nil {
c.Err(ise) c.Err(ErrISE)
return return
} }
c.log() c.log()

View file

@ -19,7 +19,7 @@ type Err struct {
// Error returns the error string // Error returns the error string
func (a Err) Error() string { func (a Err) Error() string {
return strconv.Itoa(a.status) + ": " + a.err return `{"message":"` + a.err + `","status":` + strconv.Itoa(a.status) + `}`
} }
// Status returns the HTTP status code // Status returns the HTTP status code
@ -35,11 +35,11 @@ func newError(err string, status int) Error {
} }
var ( var (
badreq = newError("bad request", http.StatusBadRequest) ErrBadreq = newError("bad request", http.StatusBadRequest)
invalid = newError("invalid data", http.StatusBadRequest) ErrInvalid = newError("invalid data", http.StatusBadRequest)
forbid = newError("forbidden", http.StatusForbidden) ErrForbidden = newError("forbidden", http.StatusForbidden)
nf = newError("resource not found", http.StatusNotFound) ErrNotFound = newError("not found", http.StatusNotFound)
mna = newError("method not allowed", http.StatusMethodNotAllowed) ErrMNA = newError("method not allowed", http.StatusMethodNotAllowed)
exists = newError("resource already exists", http.StatusConflict) ErrExists = newError("resource already exists", http.StatusConflict)
ise = newError("internal server error", http.StatusInternalServerError) ErrISE = newError("internal server error", http.StatusInternalServerError)
) )

View file

@ -1,5 +1,11 @@
package schema package schema
import (
"encoding/json"
"errors"
"fmt"
)
/* /*
healthCheckNodePort healthCheckNodePort
@ -30,3 +36,33 @@ type Server struct {
IP string `json:"ip,omitempty"` IP string `json:"ip,omitempty"`
Port int `json:"port,omitempty"` Port int `json:"port,omitempty"`
} }
func (lb LoadBalancer) Validate() error {
if lb.Name == "" {
return errors.New("LoadBalancer.Name can not be empty")
}
if lb.IP == "" {
return errors.New("LoadBalancer.IP can not be empty")
}
for i, p := range lb.Ports {
if p.Port < 0 || p.Port > 65535 {
return fmt.Errorf("LoadBalancer.Ports[%d].Port is invalid (range 0-65535", i)
}
for j, s := range p.Servers {
if s.Name == "" {
return fmt.Errorf("LoadBalancer.Ports[%d].Servers[%d].Name can not be empty", i, j)
}
if s.IP == "" {
return fmt.Errorf("LoadBalancer.Ports[%d].Servers[%d].IP can not be empty", i, j)
}
if s.Port < 0 || s.Port > 65535 {
return fmt.Errorf("LoadBalancer.Ports[%d].Servers[%d].Port is invalid (range 0-65535)", i, j)
}
}
}
return nil
}
func (lb LoadBalancer) JSON() ([]byte, error) {
return json.Marshal(lb)
}

View file

@ -3,8 +3,12 @@ package server
import ( import (
//"golang.org/x/crypto/bcrypt" //"golang.org/x/crypto/bcrypt"
//"net/http" //"net/http"
"context"
"encoding/json"
"git.giftfish.de/ston1th/haproxy-lb/pkg/api/types" "git.giftfish.de/ston1th/haproxy-lb/pkg/api/types"
"git.giftfish.de/ston1th/haproxy-lb/pkg/api/v1/schema"
"git.giftfish.de/ston1th/haproxy-lb/pkg/cluster"
) )
func authHandler(h types.CtxHandler) types.CtxHandler { func authHandler(h types.CtxHandler) types.CtxHandler {
@ -22,10 +26,115 @@ func healthzHandler(ctx *types.Context) {
ctx.OK() ctx.OK()
} }
func lbListHandler(ctx *types.Context) { func name(c, n string) string {
ctx.OK() return c + "/" + n
}
func lbname(c, n string) string {
return c + "_" + n
} }
func lbHandler(ctx *types.Context) { func lbHandler(ctx *types.Context) {
cl := ctx.Var("cluster")
n := ctx.Var("name")
name := name(cl, n)
switch ctx.Method() {
case "GET":
b, err := ctx.Data.DB.GetLB(name)
if err != nil {
ctx.Log.Error(err, "error reading loadbalancer", "cluster", cl, "name", n)
if err == cluster.ErrKeyNotFound {
ctx.Err(types.ErrNotFound)
return
}
ctx.Err(types.ErrISE)
return
}
ctx.Body(b)
case "POST":
// TODO
// handle if loadbalancer already exists
// decode existing LB and read Name + IP from there
b, err := ctx.ReadBody()
if err != nil {
ctx.Log.Error(err, "error reading request body", "cluster", cl, "name", n)
ctx.Err(types.ErrISE)
return
}
var lb schema.LoadBalancer
err = json.Unmarshal(b, &lb)
if err != nil {
ctx.Log.Error(err, "error decoding request body", "cluster", cl, "name", n)
ctx.Err(types.ErrInvalid)
return
}
lb.Name = lbname(cl, n)
lb.IP, err = ctx.Data.Alloc.AllocIP(context.Background(), name)
if err != nil {
ctx.Log.Error(err, "error allocating IP for loadbalancer", "cluster", cl, "name", n)
ctx.Err(types.ErrISE)
return
}
err = lb.Validate()
if err != nil {
ctx.Log.Error(err, "error validating loadbalancer", "cluster", cl, "name", n)
ctx.Err(types.ErrInvalid)
return
}
b, err = lb.JSON()
if err != nil {
ctx.Log.Error(err, "error encoding loadbalancer", "cluster", cl, "name", n)
ctx.Err(types.ErrISE)
return
}
err = ctx.Data.DB.SetLB(name, b)
if err != nil {
ctx.Log.Error(err, "error writing loadbalancer", "cluster", cl, "name", n)
ctx.Err(types.ErrISE)
return
}
ctx.OK() ctx.OK()
case "DELETE":
err := ctx.Data.Alloc.FreeIP(context.Background(), name)
if err != nil {
ctx.Log.Error(err, "error freeing IP of loadbalancer", "cluster", cl, "name", n)
ctx.Err(types.ErrISE)
return
}
err = ctx.Data.DB.DeleteLB(name)
if err != nil {
ctx.Log.Error(err, "error deleting loadbalancer", "cluster", cl, "name", n)
ctx.Err(types.ErrISE)
return
}
ctx.OK()
}
}
func lbList(lbs map[string][]byte) (m map[string]json.RawMessage) {
m = make(map[string]json.RawMessage)
for k, v := range lbs {
m[k] = json.RawMessage(v)
}
return
}
func lbClusterListHandler(ctx *types.Context) {
cl := ctx.Var("cluster")
m, err := ctx.Data.DB.GetLBs(cl)
if err != nil {
ctx.Log.Error(err, "error reading loadbalancers")
ctx.Err(types.ErrISE)
return
}
ctx.JSON(lbList(m))
}
func lbListHandler(ctx *types.Context) {
m, err := ctx.Data.DB.GetLBs("")
if err != nil {
ctx.Log.Error(err, "error reading loadbalancers")
ctx.Err(types.ErrISE)
return
}
ctx.JSON(lbList(m))
} }

View file

@ -18,6 +18,11 @@ var Routes = []types.Route{
authHandler(lbListHandler), authHandler(lbListHandler),
[]string{"GET"}, []string{"GET"},
}, },
{
v1 + "/lb/{cluster:[a-zA-Z0-9-]+$}",
authHandler(lbClusterListHandler),
[]string{"GET"},
},
{ {
v1 + "/lb/{cluster:[a-zA-Z0-9-]+}/{name:[a-zA-Z0-9-]+$}", v1 + "/lb/{cluster:[a-zA-Z0-9-]+}/{name:[a-zA-Z0-9-]+$}",
authHandler(lbHandler), authHandler(lbHandler),

View file

@ -19,7 +19,7 @@ const (
EtcdLogLevel = "error" EtcdLogLevel = "error"
EtcdPrefix = "/haproxy-lb" EtcdPrefix = "/haproxy-lb"
HAProxyConfigFile = "/etc/haproxy/haproxy.cf" HAProxyConfigFile = "/etc/haproxy/haproxy.cfg"
) )
func ParseFile(file string) (cfg *Config, err error) { func ParseFile(file string) (cfg *Config, err error) {

View file

@ -51,16 +51,22 @@ func NewLBController(cfg *config.Config, srv *api.Server, log logr.Logger) (call
continue continue
} }
addIPs(cc, n, ips) addIPs(cc, n, ips)
// TODO // TODO
//cfg, err := cc.Get("config") // deleted LB IPs need to be removed
//if err != nil {
// cc.Error(err, "error getting config key") lbs, err := db.GetLBs("")
// <-t.C if err != nil && err != cluster.ErrPrefixNotFound {
// continue cc.Error(err, "error reading lb list")
//} <-t.C
var lbs haproxy.Config continue
err = ha.UpdateConfig(ctx, lbs) }
lbcfg, err := haproxy.NewConfig(lbs)
if err != nil {
cc.Error(err, "error reading haproxy config")
<-t.C
continue
}
err = ha.UpdateConfig(ctx, lbcfg)
if err != nil { if err != nil {
cc.Error(err, "error updating haproxy config") cc.Error(err, "error updating haproxy config")
<-t.C <-t.C

View file

@ -15,6 +15,7 @@ func New(kv cluster.KV) *DB {
const ( const (
ipPrefix = "/ip/" ipPrefix = "/ip/"
namePrefix = "/name/" namePrefix = "/name/"
lbPrefix = "/lb/"
) )
func (db *DB) GetIPs() (ips []string, err error) { func (db *DB) GetIPs() (ips []string, err error) {
@ -54,3 +55,31 @@ func (db *DB) GetName(name string) (string, error) {
v, err := db.kv.Get(namePrefix + name) v, err := db.kv.Get(namePrefix + name)
return string(v), err return string(v), err
} }
func (db *DB) GetLBs(cl string) (lbs map[string][]byte, err error) {
lbs = make(map[string][]byte)
if cl != "" {
cl = cl + "/"
}
m, err := db.kv.GetPrefix(lbPrefix + cl)
if err == cluster.ErrPrefixNotFound {
err = nil
return
}
if err != nil {
return
}
for k, v := range m {
lbs[k] = v
}
return
}
func (db *DB) GetLB(name string) ([]byte, error) {
return db.kv.Get(lbPrefix + name)
}
func (db *DB) SetLB(name string, lb []byte) error {
return db.kv.Set(lbPrefix+name, lb)
}
func (db *DB) DeleteLB(name string) error {
return db.kv.Delete(lbPrefix + name)
}

View file

@ -10,11 +10,6 @@ import (
clientv3 "go.etcd.io/etcd/client/v3" clientv3 "go.etcd.io/etcd/client/v3"
) )
var (
ErrKeyNotFound = errors.New("etcd: key not found")
ErrPrefixNotFound = errors.New("etcd: prefix not found")
)
type Cluster struct { type Cluster struct {
logr.Logger logr.Logger
cli *clientv3.Client cli *clientv3.Client
@ -56,8 +51,8 @@ func (c *Cluster) Get(k string) (v []byte, err error) {
return return
} }
func (c *Cluster) GetPrefix(k string) (m map[string][]byte, err error) { func (c *Cluster) GetPrefix(pk string) (m map[string][]byte, err error) {
r, err := c.cli.Get(c.ctx, c.kvPrefix+"/"+k, clientv3.WithPrefix()) r, err := c.cli.Get(c.ctx, c.kvPrefix+"/"+pk, clientv3.WithPrefix())
if err != nil { if err != nil {
return return
} }
@ -67,8 +62,8 @@ func (c *Cluster) GetPrefix(k string) (m map[string][]byte, err error) {
} }
m = make(map[string][]byte) m = make(map[string][]byte)
for _, kv := range r.Kvs { for _, kv := range r.Kvs {
k := strings.TrimPrefix(c.kvPrefix+"/", string(kv.Key)) key := strings.TrimPrefix(string(kv.Key), c.kvPrefix+"/"+pk)
m[k] = kv.Value m[key] = kv.Value
} }
return return
} }

View file

@ -1,29 +1,43 @@
package haproxy package haproxy
import "encoding/json"
type Config struct { type Config struct {
LoadBalancers []LoadBalancer LoadBalancers []*LoadBalancer
}
func NewConfig(m map[string][]byte) (c Config, err error) {
for _, v := range m {
lb := new(LoadBalancer)
err = json.Unmarshal(v, lb)
if err != nil {
return
}
c.LoadBalancers = append(c.LoadBalancers, lb)
}
return
} }
type LoadBalancer struct { type LoadBalancer struct {
Name string Name string `json:"name"`
IP string IP string `json:"ip"`
Options Options Options Options `json:"options"`
Ports []Port Ports []Port `json:"ports"`
} }
type Options struct { type Options struct {
Frontend []string Frontend []string `json:"frontend"`
Backend []string Backend []string `json:"backend"`
DefaultServer string DefaultServer string `json:"defaultServer"`
} }
type Port struct { type Port struct {
Port int Port int `json:"port"`
Servers []Server Servers []Server `json:"servers"`
} }
type Server struct { type Server struct {
Name string Name string `json:"name"`
IP string IP string `json:"ip"`
Port int Port int `json:"port"`
} }

View file

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/sha256" "crypto/sha256"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
@ -51,7 +52,11 @@ func (ha *HAProxyManager) checkConfig(ctx context.Context, lbs Config) (hash []b
return return
} }
hash = h.Sum(nil) hash = h.Sum(nil)
return hash, exec.CommandContext(ctx, "haproxy", "-c", "-f", tmp.Name()).Run() out, err := exec.CommandContext(ctx, "haproxy", "-c", "-f", tmp.Name()).CombinedOutput()
if err != nil {
err = fmt.Errorf("%s:%w", string(out), err)
}
return hash, err
} }
func (ha *HAProxyManager) UpdateConfig(ctx context.Context, lbs Config) error { func (ha *HAProxyManager) UpdateConfig(ctx context.Context, lbs Config) error {

View file

@ -2,10 +2,12 @@ package haproxy
const haproxyConfigTemplate = `global const haproxyConfigTemplate = `global
daemon daemon
chroot /var/lib/haproxy #chroot /var/lib/haproxy
chroot /usr/share/haproxy
pidfile /run/haproxy.pid
user haproxy user haproxy
group haproxy group haproxy
maxconn 5000 maxconn 20000
defaults defaults
mode tcp mode tcp
log global log global
@ -15,29 +17,37 @@ defaults
timeout client 5000ms timeout client 5000ms
timeout tunnel 3600s timeout tunnel 3600s
timeout check 10s timeout check 10s
frontend stats
mode http
bind 127.0.0.1:8404
stats enable
stats uri /stats
stats refresh 10s
stats admin if LOCALHOST
{{ range $item := .LoadBalancers -}} {{ 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 }}
{{ range $opt := $item.Options.Frontend -}} {{- range $opt := $item.Options.Frontend }}
option {{ $opt }} option {{ $opt }}
{{ end -}} {{- end }}
# option tcplog # option tcplog
# option splice-request # option splice-request
default_backend ba_{{ $item.Name }}_{{ $port.Port }} default_backend ba_{{ $item.Name }}_{{ $port.Port }}
backend ba_{{ $item.Name }}_{{ $port.Port }} backend ba_{{ $item.Name }}_{{ $port.Port }}
balance roundrobin balance roundrobin
{{ range $opt := $item.Options.Backend -}} {{- range $opt := $item.Options.Backend }}
option {{ $opt }} option {{ $opt }}
{{ end -}} {{- end }}
# option splice-response # option splice-response
# option httpchk GET / # option httpchk GET /
default-server inter 3s downinter 10s fall 2 rise 2 on-marked-down shutdown-sessions {{ $item.Options.DefaultServer }} default-server inter 3s downinter 10s fall 2 rise 2 on-marked-down shutdown-sessions {{ $item.Options.DefaultServer }}
# send-proxy-v2 # send-proxy-v2
# check-send-proxy # check-send-proxy
# port <healthCheckNodePort> # port <healthCheckNodePort>
{{ range $srv := $port.Servers -}} {{- range $srv := $port.Servers }}
server {{ $srv.Name }} {{ $srv.IP }}:{{ $srv.Port }} check server {{ $srv.Name }} {{ $srv.IP }}:{{ $srv.Port }} check
{{ end -}} {{- end }}
{{ end -}} {{ end -}}
{{ end -}}` {{ end -}}
`

View file

@ -33,7 +33,8 @@ func (kv *kv) GetPrefix(pk string) (m map[string][]byte, err error) {
m = make(map[string][]byte) m = make(map[string][]byte)
for k, v := range kv.m { for k, v := range kv.m {
if strings.HasPrefix(k, pk) { if strings.HasPrefix(k, pk) {
m[k] = v key := strings.TrimPrefix(k, pk)
m[key] = v
} }
} }
if len(m) == 0 { if len(m) == 0 {