Skip to content

Commit d618eb5

Browse files
committed
FEATURE/MEDIUM: runtime: add maps support
1 parent 7db24f4 commit d618eb5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+9215
-2462
lines changed

CONTRIBUTING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ Data Plane API is generated using [go-swagger](https://github.com/go-swagger/go-
4141
--tags=ACL \
4242
--tags=Defaults \
4343
--tags=StickTable \
44+
--tags=Maps \
45+
--tags=Nameserver \
46+
--tags=Cluster \
47+
--tags=Peer \
48+
--tags=PeerEntry \
49+
--tags=Resolver \
4450
-r ~/dataplaneapi-specification/copyright.txt
4551
```
4652

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ Application Options:
5353
--tls-keep-alive= sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)
5454
--tls-read-timeout= maximum duration before timing out read of the request
5555
--tls-write-timeout= maximum duration before timing out write of the response
56+
-p, --maps-dir= path to maps directory (default:"/etc/haproxy/maps")
57+
--update-map-files= flag used for periodic syncing map files entries with runtime maps entries. Missing runtime entries are appended to the corresponding map file
58+
--update-map-files-period= elapsed time in seconds between two maps syncing operations (default: 10s)
5659
5760
HAProxy options:
5861
-c, --config-file= Path to the haproxy configuration file (default: /etc/haproxy/haproxy.cfg)

configuration/configuration.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,23 @@ import (
3333
var cfg *Configuration
3434

3535
type HAProxyConfiguration struct {
36-
ConfigFile string `short:"c" long:"config-file" description:"Path to the haproxy configuration file" default:"/etc/haproxy/haproxy.cfg"`
37-
Userlist string `short:"u" long:"userlist" description:"Userlist in HAProxy configuration to use for API Basic Authentication" default:"controller"`
38-
HAProxy string `short:"b" long:"haproxy-bin" description:"Path to the haproxy binary file" default:"haproxy"`
39-
ReloadDelay int `short:"d" long:"reload-delay" description:"Minimum delay between two reloads (in s)" default:"5"`
40-
ReloadCmd string `short:"r" long:"reload-cmd" description:"Reload command"`
41-
RestartCmd string `short:"s" long:"restart-cmd" description:"Restart command"`
42-
ReloadRetention int `long:"reload-retention" description:"Reload retention in days, every older reload id will be deleted" default:"1"`
43-
TransactionDir string `short:"t" long:"transaction-dir" description:"Path to the transaction directory" default:"/tmp/haproxy"`
44-
BackupsNumber int `short:"n" long:"backups-number" description:"Number of backup configuration files you want to keep, stored in the config dir with version number suffix" default:"0"`
45-
MasterRuntime string `short:"m" long:"master-runtime" description:"Path to the master Runtime API socket"`
46-
ShowSystemInfo bool `short:"i" long:"show-system-info" description:"Show system info on info endpoint"`
47-
DataplaneConfig string `short:"f" description:"Path to the dataplane configuration file" default:"" yaml:"-"`
48-
UserListFile string `long:"userlist-file" description:"Path to the dataplaneapi userlist file. By default userlist is read from HAProxy conf. When specified userlist would be read from this file"`
49-
NodeIDFile string `long:"fid" description:"Path to file that will dataplaneapi use to write its id (not a pid) that was given to him after joining a cluster"`
36+
ConfigFile string `short:"c" long:"config-file" description:"Path to the haproxy configuration file" default:"/etc/haproxy/haproxy.cfg"`
37+
Userlist string `short:"u" long:"userlist" description:"Userlist in HAProxy configuration to use for API Basic Authentication" default:"controller"`
38+
HAProxy string `short:"b" long:"haproxy-bin" description:"Path to the haproxy binary file" default:"haproxy"`
39+
ReloadDelay int `short:"d" long:"reload-delay" description:"Minimum delay between two reloads (in s)" default:"5"`
40+
ReloadCmd string `short:"r" long:"reload-cmd" description:"Reload command"`
41+
RestartCmd string `short:"s" long:"restart-cmd" description:"Restart command"`
42+
ReloadRetention int `long:"reload-retention" description:"Reload retention in days, every older reload id will be deleted" default:"1"`
43+
TransactionDir string `short:"t" long:"transaction-dir" description:"Path to the transaction directory" default:"/tmp/haproxy"`
44+
BackupsNumber int `short:"n" long:"backups-number" description:"Number of backup configuration files you want to keep, stored in the config dir with version number suffix" default:"0"`
45+
MasterRuntime string `short:"m" long:"master-runtime" description:"Path to the master Runtime API socket"`
46+
ShowSystemInfo bool `short:"i" long:"show-system-info" description:"Show system info on info endpoint"`
47+
DataplaneConfig string `short:"f" description:"Path to the dataplane configuration file" default:"" yaml:"-"`
48+
UserListFile string `long:"userlist-file" description:"Path to the dataplaneapi userlist file. By default userlist is read from HAProxy conf. When specified userlist would be read from this file"`
49+
NodeIDFile string `long:"fid" description:"Path to file that will dataplaneapi use to write its id (not a pid) that was given to him after joining a cluster"`
50+
MapsDir string `short:"p" long:"maps-dir" description:"Path to maps directory" default:"/etc/haproxy/maps"`
51+
UpdateMapFiles bool `long:"update-map-files" description:"Flag used for syncing map files with runtime maps values"`
52+
UpdateMapFilesPeriod int64 `long:"update-map-files-period" description:"Elapsed time in seconds between two maps syncing operations" default:"10"`
5053
}
5154

5255
type LoggingOptions struct {

configure_data_plane.go

Lines changed: 186 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import (
2121
"crypto/tls"
2222
"encoding/json"
2323
"fmt"
24+
"hash/fnv"
25+
"io/ioutil"
26+
"math/rand"
2427
"net/http"
2528
"os"
2629
"os/signal"
@@ -29,9 +32,11 @@ import (
2932
"strconv"
3033
"strings"
3134
"syscall"
35+
"time"
3236

3337
"github.com/haproxytech/dataplaneapi/adapters"
3438
"github.com/haproxytech/dataplaneapi/operations/specification"
39+
"github.com/haproxytech/models"
3540

3641
log "github.com/sirupsen/logrus"
3742

@@ -140,6 +145,11 @@ func configureAPI(api *operations.DataPlaneAPI) http.Handler {
140145
signal.Notify(sigs, syscall.SIGUSR1, syscall.SIGUSR2)
141146
go handleSignals(sigs, client, haproxyOptions, users)
142147

148+
// Sync map physical file with runtime map entries
149+
if haproxyOptions.UpdateMapFiles {
150+
go syncMaps(client)
151+
}
152+
143153
// Initialize reload agent
144154
ra := &haproxy.ReloadAgent{}
145155
if err := ra.Init(haproxyOptions.ReloadDelay, haproxyOptions.ReloadCmd, haproxyOptions.RestartCmd, haproxyOptions.ConfigFile, haproxyOptions.ReloadRetention); err != nil {
@@ -382,6 +392,17 @@ func configureAPI(api *operations.DataPlaneAPI) http.Handler {
382392
api.StickTableGetStickTableHandler = &handlers.GetStickTableHandlerImpl{Client: client}
383393
api.StickTableGetStickTableEntriesHandler = &handlers.GetStickTableEntriesHandlerImpl{Client: client}
384394

395+
// setup map handlers
396+
api.MapsCreateRuntimeMapHandler = &handlers.MapsCreateRuntimeMapHandlerImpl{Client: client}
397+
api.MapsGetAllRuntimeMapFilesHandler = &handlers.GetMapsHandlerImpl{Client: client}
398+
api.MapsGetOneRuntimeMapHandler = &handlers.GetMapHandlerImpl{Client: client}
399+
api.MapsClearRuntimeMapHandler = &handlers.ClearMapHandlerImpl{Client: client}
400+
api.MapsShowRuntimeMapHandler = &handlers.ShowMapHandlerImpl{Client: client}
401+
api.MapsAddMapEntryHandler = &handlers.AddMapEntryHandlerImpl{Client: client}
402+
api.MapsGetRuntimeMapEntryHandler = &handlers.GetRuntimeMapEntryHandlerImpl{Client: client}
403+
api.MapsReplaceRuntimeMapEntryHandler = &handlers.ReplaceRuntimeMapEntryHandlerImpl{Client: client}
404+
api.MapsDeleteRuntimeMapEntryHandler = &handlers.DeleteRuntimeMapEntryHandlerImpl{Client: client}
405+
385406
// setup info handler
386407
api.InformationGetInfoHandler = &handlers.GetInfoHandlerImpl{SystemInfo: haproxyOptions.ShowSystemInfo, BuildTime: BuildTime, Version: Version}
387408

@@ -534,6 +555,7 @@ func serverShutdown() {
534555
if logFile != nil {
535556
logFile.Close()
536557
}
558+
MapQuitChan <- MapQuitNotice{}
537559
}
538560

539561
func configureNativeClient(haproxyOptions dataplaneapi_config.HAProxyConfiguration, mWorker bool) *client_native.HAProxyClient {
@@ -575,7 +597,11 @@ func configureConfigurationClient(haproxyOptions dataplaneapi_config.HAProxyConf
575597
}
576598

577599
func configureRuntimeClient(confClient *configuration.Client, haproxyOptions dataplaneapi_config.HAProxyConfiguration) *runtime_api.Client {
578-
runtimeClient := &runtime_api.Client{}
600+
runtimeParams := runtime_api.ClientParams{
601+
MapsDir: haproxyOptions.MapsDir,
602+
}
603+
runtimeClient := &runtime_api.Client{ClientParams: runtimeParams}
604+
579605
_, globalConf, err := confClient.GetGlobalConfiguration("")
580606

581607
// First try to setup master runtime socket
@@ -584,7 +610,7 @@ func configureRuntimeClient(confClient *configuration.Client, haproxyOptions dat
584610
// If master socket is set and a valid unix socket, use only this
585611
if haproxyOptions.MasterRuntime != "" && misc.IsUnixSocketAddr(haproxyOptions.MasterRuntime) {
586612
masterSocket := haproxyOptions.MasterRuntime
587-
// if nbproc is set set nbproc sockets
613+
// if nbproc is set, set nbproc sockets
588614
if globalConf.Nbproc > 0 {
589615
nbproc := int(globalConf.Nbproc)
590616
if err = runtimeClient.InitWithMasterSocket(masterSocket, nbproc); err == nil {
@@ -672,3 +698,161 @@ func handleSignals(sigs chan os.Signal, client *client_native.HAProxyClient, hap
672698
}
673699
}
674700
}
701+
702+
type MapQuitNotice struct{}
703+
704+
var MapQuitChan = make(chan MapQuitNotice)
705+
706+
//syncMaps sync maps file entries with runtime maps entries for all configured files.
707+
//Missing runtime entries are appended to the map file
708+
func syncMaps(client *client_native.HAProxyClient) {
709+
cfg := dataplaneapi_config.Get()
710+
haproxyOptions := cfg.HAProxy
711+
712+
d := time.Duration(haproxyOptions.UpdateMapFilesPeriod)
713+
ticker := time.NewTicker(d * time.Second)
714+
715+
for {
716+
select {
717+
case <-ticker.C:
718+
maps, err := client.Runtime.ShowMaps()
719+
if err != nil {
720+
log.Warning("syncMaps runtime API ShowMaps error: ", err.Error())
721+
}
722+
for _, mp := range maps {
723+
go syncMapFilesToRuntimeEntries(mp, client)
724+
}
725+
case <-MapQuitChan:
726+
return
727+
}
728+
}
729+
}
730+
731+
func syncMapFilesToRuntimeEntries(mp *models.Map, client *client_native.HAProxyClient) {
732+
//map file entries
733+
raw, err := ioutil.ReadFile(mp.File)
734+
if err != nil {
735+
log.Warningf("error reading map file: %s %s", mp.File, err.Error())
736+
}
737+
fileEntries := client.Runtime.ParseMapEntries(string(raw))
738+
739+
//runtime map entries
740+
id := fmt.Sprintf("#%s", mp.ID)
741+
runtimeEntries, err := client.Runtime.ShowMapEntries(id)
742+
if err != nil {
743+
log.Warningf("error runtime API ShowMapEntries: id: %s %s", id, err.Error())
744+
}
745+
746+
if len(runtimeEntries) < 1 {
747+
return
748+
}
749+
750+
if len(fileEntries) != len(runtimeEntries) {
751+
if dumpRuntimeEntries(mp.File, runtimeEntries) {
752+
log.Infof("map file %s synced with runtime entries", mp.File)
753+
return
754+
}
755+
}
756+
757+
if !equalSomeFileAndRuntimeEntries(fileEntries, runtimeEntries) {
758+
if dumpRuntimeEntries(mp.File, runtimeEntries) {
759+
log.Infof("map file %s synced with runtime entries", mp.File)
760+
return
761+
}
762+
}
763+
764+
if !equalHashFileAndRuntimeEntries(fileEntries, runtimeEntries) {
765+
if dumpRuntimeEntries(mp.File, runtimeEntries) {
766+
log.Infof("map file %s synced with runtime entries", mp.File)
767+
return
768+
}
769+
}
770+
}
771+
772+
//equalSomeFileAndRuntimeEntries compares last few runtime entries with file entries
773+
//if records differs, check is run against random entries
774+
func equalSomeFileAndRuntimeEntries(fEntries, rEntries models.MapEntries) bool {
775+
if len(fEntries) != len(rEntries) {
776+
return false
777+
}
778+
779+
max := 0
780+
switch l := len(rEntries); {
781+
case l > 19:
782+
for i := l - 20; i < l; i++ {
783+
if rEntries[i].Key != fEntries[i].Key || rEntries[i].Value != fEntries[i].Value {
784+
return false
785+
}
786+
}
787+
max = l - 19
788+
default:
789+
max = l
790+
}
791+
792+
for i := 0; i < 10; i++ {
793+
rand.Seed(time.Now().UTC().UnixNano())
794+
r := rand.Intn(max-1) + 1
795+
if rEntries[r].Key != fEntries[r].Key || rEntries[r].Value != fEntries[r].Value {
796+
return false
797+
}
798+
}
799+
return true
800+
}
801+
802+
func hash(s string) uint32 {
803+
h := fnv.New32a()
804+
_, _ = h.Write([]byte(s))
805+
return h.Sum32()
806+
}
807+
808+
//equalHashFileAndRuntimeEntries compares runtime and map entries in form of hash
809+
//Returns true if hashes are same, otherwise returns false
810+
func equalHashFileAndRuntimeEntries(fEntries, rEntries models.MapEntries) bool {
811+
if len(fEntries) != len(rEntries) {
812+
return false
813+
}
814+
815+
var fb strings.Builder
816+
for _, fe := range fEntries {
817+
fb.WriteString(fe.Key + fe.Value)
818+
}
819+
820+
var rb strings.Builder
821+
for _, re := range rEntries {
822+
rb.WriteString(re.Key + re.Value)
823+
}
824+
825+
return hash(fb.String()) == hash(rb.String())
826+
}
827+
828+
//dumpRuntimeEntries dumps runtime entries into map file
829+
func dumpRuntimeEntries(file string, me models.MapEntries) bool {
830+
f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_TRUNC, 0600)
831+
if err != nil {
832+
log.Warningf("error opening map file: %s %s", file, err.Error())
833+
return false
834+
}
835+
defer f.Close()
836+
837+
err = f.Truncate(0)
838+
if err != nil {
839+
log.Warningf("error truncating map file: %s %s", file, err.Error())
840+
return false
841+
}
842+
843+
_, err = f.Seek(0, 0)
844+
if err != nil {
845+
log.Warningf("error setting file to offset: %s %s", file, err.Error())
846+
return false
847+
}
848+
849+
for _, e := range me {
850+
line := fmt.Sprintf("%s %s%s", e.Key, e.Value, "\n")
851+
_, err = f.WriteString(line)
852+
if err != nil {
853+
log.Warningf("error writing map file: %s %s", file, err.Error())
854+
return false
855+
}
856+
}
857+
return true
858+
}

doc.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)