diff --git a/docs/content/en/docs/configuration/command-line.md b/docs/content/en/docs/configuration/command-line.md index 533a104c6..8a9358346 100644 --- a/docs/content/en/docs/configuration/command-line.md +++ b/docs/content/en/docs/configuration/command-line.md @@ -20,6 +20,7 @@ The following command-line options are supported: | [`--acme-track-tls-annotation`](#acme) | [true\|false] | `false` | v0.9 | | [`--allow-cross-namespace`](#allow-cross-namespace) | [true\|false] | `false` | | | [`--annotation-prefix`](#annotation-prefix) | prefix without `/` | `ingress.kubernetes.io` | v0.8 | +| [`--backend-shards`](#backend-shards) | int | `0` | v0.11 | | [`--buckets-response-time`](#buckets-response-time) | float64 slice | `.0005,.001,.002,.005,.01` | v0.10 | | [`--default-backend-service`](#default-backend-service) | namespace/servicename | haproxy's 404 page | | | [`--default-ssl-certificate`](#default-ssl-certificate) | namespace/secretname | fake, auto generated | | @@ -82,6 +83,16 @@ that shares ingress and service objects without conflicting each other. --- +## --backend-shards + +Defines how much files should be used to configure the haproxy backends. The default value is +0 (zero) which uses one single file to configure the whole haproxy process. Values greather than +0 (zero) splits the backend configuration into separated files. Only files with changed backends +are parsed and written to disk, reducing io and cpu usage on big clusters - about 1000 or more +services. + +--- + ## --buckets-response-time Configures the buckets of the histogram `haproxyingress_haproxy_response_time_seconds`, used to compute the response time of the haproxy's admin socket. The response time unit is in seconds. diff --git a/pkg/common/ingress/controller/controller.go b/pkg/common/ingress/controller/controller.go index 345a7fdc8..929217970 100644 --- a/pkg/common/ingress/controller/controller.go +++ b/pkg/common/ingress/controller/controller.go @@ -94,6 +94,7 @@ type Configuration struct { ElectionID string UpdateStatusOnShutdown bool + BackendShards int SortBackends bool IgnoreIngressWithoutClass bool } diff --git a/pkg/common/ingress/controller/launch.go b/pkg/common/ingress/controller/launch.go index d4fc1729d..7accc60a5 100644 --- a/pkg/common/ingress/controller/launch.go +++ b/pkg/common/ingress/controller/launch.go @@ -152,6 +152,9 @@ func NewIngressController(backend ingress.Controller) *GenericController { ingress controller should update the Ingress status IP/hostname when the controller is being stopped. Default is true`) + backendShards = flags.Int("backend-shards", 0, + `Defines how much files should be used to configure the haproxy backends`) + sortBackends = flags.Bool("sort-backends", false, `Defines if backends and it's endpoints should be sorted`) @@ -311,6 +314,7 @@ func NewIngressController(backend ingress.Controller) *GenericController { AllowCrossNamespace: *allowCrossNamespace, DisableNodeList: *disableNodeList, UpdateStatusOnShutdown: *updateStatusOnShutdown, + BackendShards: *backendShards, SortBackends: *sortBackends, UseNodeInternalIP: *useNodeInternalIP, IgnoreIngressWithoutClass: *ignoreIngressWithoutClass, diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index c9d3a6c9f..07f4aedf9 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -120,7 +120,9 @@ func (hc *HAProxyController) configController() { instanceOptions := haproxy.InstanceOptions{ HAProxyCmd: "haproxy", ReloadCmd: "/haproxy-reload.sh", - HAProxyConfigFile: "/etc/haproxy/haproxy.cfg", + HAProxyCfgDir: "/etc/haproxy", + HAProxyMapsDir: "/etc/haproxy/maps", + BackendShards: hc.cfg.BackendShards, AcmeSigner: acmeSigner, AcmeQueue: hc.acmeQueue, LeaderElector: hc.leaderelector, diff --git a/pkg/haproxy/config.go b/pkg/haproxy/config.go index 7a6b06068..c5ac1d89d 100644 --- a/pkg/haproxy/config.go +++ b/pkg/haproxy/config.go @@ -45,9 +45,8 @@ type Config interface { type config struct { // external state, non haproxy data - acmeData *hatypes.AcmeData - mapsTemplate *template.Config - mapsDir string + options options + acmeData *hatypes.AcmeData // haproxy internal state globalOld *hatypes.Global global *hatypes.Global @@ -59,26 +58,24 @@ type config struct { } type options struct { - // reflect changes to config.Clear() mapsTemplate *template.Config mapsDir string + shardCount int } func createConfig(options options) *config { - mapsTemplate := options.mapsTemplate - if mapsTemplate == nil { - mapsTemplate = template.CreateConfig() + if options.mapsTemplate == nil { + options.mapsTemplate = template.CreateConfig() } return &config{ - acmeData: &hatypes.AcmeData{}, - global: &hatypes.Global{}, - frontend: &hatypes.Frontend{Name: "_front001"}, - hosts: hatypes.CreateHosts(), - backends: hatypes.CreateBackends(), - tcpbackends: hatypes.CreateTCPBackends(), - userlists: hatypes.CreateUserlists(), - mapsTemplate: mapsTemplate, - mapsDir: options.mapsDir, + options: options, + acmeData: &hatypes.AcmeData{}, + global: &hatypes.Global{}, + frontend: &hatypes.Frontend{Name: "_front001"}, + hosts: hatypes.CreateHosts(), + backends: hatypes.CreateBackends(options.shardCount), + tcpbackends: hatypes.CreateTCPBackends(), + userlists: hatypes.CreateUserlists(), } } @@ -143,23 +140,24 @@ func (c *config) SyncConfig() { // link to the frontend maps. func (c *config) WriteFrontendMaps() error { mapBuilder := hatypes.CreateMaps() + mapsDir := c.options.mapsDir fmaps := &hatypes.FrontendMaps{ - HTTPFrontsMap: mapBuilder.AddMap(c.mapsDir + "/_global_http_front.map"), - HTTPRootRedirMap: mapBuilder.AddMap(c.mapsDir + "/_global_http_root_redir.map"), - HTTPSRedirMap: mapBuilder.AddMap(c.mapsDir + "/_global_https_redir.map"), - SSLPassthroughMap: mapBuilder.AddMap(c.mapsDir + "/_global_sslpassthrough.map"), - VarNamespaceMap: mapBuilder.AddMap(c.mapsDir + "/_global_k8s_ns.map"), + HTTPFrontsMap: mapBuilder.AddMap(mapsDir + "/_global_http_front.map"), + HTTPRootRedirMap: mapBuilder.AddMap(mapsDir + "/_global_http_root_redir.map"), + HTTPSRedirMap: mapBuilder.AddMap(mapsDir + "/_global_https_redir.map"), + SSLPassthroughMap: mapBuilder.AddMap(mapsDir + "/_global_sslpassthrough.map"), + VarNamespaceMap: mapBuilder.AddMap(mapsDir + "/_global_k8s_ns.map"), // - HostBackendsMap: mapBuilder.AddMap(c.mapsDir + "/_front001_host.map"), - RootRedirMap: mapBuilder.AddMap(c.mapsDir + "/_front001_root_redir.map"), - SNIBackendsMap: mapBuilder.AddMap(c.mapsDir + "/_front001_sni.map"), - TLSInvalidCrtErrorList: mapBuilder.AddMap(c.mapsDir + "/_front001_inv_crt.list"), - TLSInvalidCrtErrorPagesMap: mapBuilder.AddMap(c.mapsDir + "/_front001_inv_crt_redir.map"), - TLSNoCrtErrorList: mapBuilder.AddMap(c.mapsDir + "/_front001_no_crt.list"), - TLSNoCrtErrorPagesMap: mapBuilder.AddMap(c.mapsDir + "/_front001_no_crt_redir.map"), + HostBackendsMap: mapBuilder.AddMap(mapsDir + "/_front001_host.map"), + RootRedirMap: mapBuilder.AddMap(mapsDir + "/_front001_root_redir.map"), + SNIBackendsMap: mapBuilder.AddMap(mapsDir + "/_front001_sni.map"), + TLSInvalidCrtErrorList: mapBuilder.AddMap(mapsDir + "/_front001_inv_crt.list"), + TLSInvalidCrtErrorPagesMap: mapBuilder.AddMap(mapsDir + "/_front001_inv_crt_redir.map"), + TLSNoCrtErrorList: mapBuilder.AddMap(mapsDir + "/_front001_no_crt.list"), + TLSNoCrtErrorPagesMap: mapBuilder.AddMap(mapsDir + "/_front001_no_crt_redir.map"), // - CrtList: mapBuilder.AddMap(c.mapsDir + "/_front001_bind_crt.list"), - UseServerList: mapBuilder.AddMap(c.mapsDir + "/_front001_use_server.list"), + CrtList: mapBuilder.AddMap(mapsDir + "/_front001_bind_crt.list"), + UseServerList: mapBuilder.AddMap(mapsDir + "/_front001_use_server.list"), } fmaps.CrtList.AppendItem(c.frontend.DefaultCert) // Some maps use yes/no answers instead of a list with found/missing keys @@ -283,7 +281,7 @@ func (c *config) WriteFrontendMaps() error { fmaps.CrtList.AppendItem(crtListEntry) } } - if err := writeMaps(mapBuilder, c.mapsTemplate); err != nil { + if err := writeMaps(mapBuilder, c.options.mapsTemplate); err != nil { return err } c.frontend.Maps = fmaps @@ -299,7 +297,7 @@ func (c *config) WriteBackendMaps() error { mapBuilder := hatypes.CreateMaps() for _, backend := range c.backends.Items() { if backend.NeedACL() { - mapsPrefix := c.mapsDir + "/_back_" + backend.ID + mapsPrefix := c.options.mapsDir + "/_back_" + backend.ID pathsMap := mapBuilder.AddMap(mapsPrefix + "_idpath.map") for _, path := range backend.Paths { pathsMap.AppendPath(path.Hostpath, path.ID) @@ -307,7 +305,7 @@ func (c *config) WriteBackendMaps() error { backend.PathsMap = pathsMap } } - return writeMaps(mapBuilder, c.mapsTemplate) + return writeMaps(mapBuilder, c.options.mapsTemplate) } func writeMaps(maps *hatypes.HostsMaps, template *template.Config) error { @@ -349,10 +347,7 @@ func (c *config) Userlists() *hatypes.Userlists { } func (c *config) Clear() { - config := createConfig(options{ - mapsTemplate: c.mapsTemplate, - mapsDir: c.mapsDir, - }) + config := createConfig(c.options) *c = *config } diff --git a/pkg/haproxy/config_test.go b/pkg/haproxy/config_test.go index d47da07f4..46ae25a65 100644 --- a/pkg/haproxy/config_test.go +++ b/pkg/haproxy/config_test.go @@ -65,7 +65,7 @@ func TestClear(t *testing.T) { }) c.Hosts().AcquireHost("app.local") c.Backends().AcquireBackend("default", "app", "8080") - if c.mapsDir != "/tmp/maps" { + if c.options.mapsDir != "/tmp/maps" { t.Error("expected mapsDir == /tmp/maps") } if len(c.Hosts().Items()) != 1 { @@ -75,7 +75,7 @@ func TestClear(t *testing.T) { t.Error("expected len(backends) == 1") } c.Clear() - if c.mapsDir != "/tmp/maps" { + if c.options.mapsDir != "/tmp/maps" { t.Error("expected mapsDir == /tmp/maps") } if len(c.Hosts().Items()) != 0 { diff --git a/pkg/haproxy/dynupdate_test.go b/pkg/haproxy/dynupdate_test.go index cdaabcdef..8bc56dbe8 100644 --- a/pkg/haproxy/dynupdate_test.go +++ b/pkg/haproxy/dynupdate_test.go @@ -585,11 +585,10 @@ set server default_app_8080/srv002 weight 1`, } for i, test := range testCases { c := setup(t) - instance := c.instance.(*instance) if test.doconfig1 != nil { test.doconfig1(c) } - instance.config.Commit() + c.instance.config.Commit() backendIDs := []types.BackendID{} for _, backend := range c.config.Backends().Items() { if backend != c.config.Backends().DefaultBackend() { @@ -601,7 +600,7 @@ set server default_app_8080/srv002 weight 1`, test.doconfig2(c) } var cmd string - dynUpdater := instance.newDynUpdater() + dynUpdater := c.instance.newDynUpdater() dynUpdater.cmd = func(socket string, observer func(duration time.Duration), command ...string) ([]string, error) { for _, c := range command { cmd = cmd + c + "\n" diff --git a/pkg/haproxy/instance.go b/pkg/haproxy/instance.go index 9907de78a..42ec95b9d 100644 --- a/pkg/haproxy/instance.go +++ b/pkg/haproxy/instance.go @@ -19,6 +19,7 @@ package haproxy import ( "fmt" "os/exec" + "path/filepath" "regexp" "strconv" "strings" @@ -35,10 +36,12 @@ import ( type InstanceOptions struct { AcmeSigner acme.Signer AcmeQueue utils.Queue + BackendShards int + HAProxyCmd string + HAProxyCfgDir string + HAProxyMapsDir string LeaderElector types.LeaderElector MaxOldConfigFiles int - HAProxyCmd string - HAProxyConfigFile string Metrics types.Metrics ReloadCmd string ReloadStrategy string @@ -57,24 +60,24 @@ type Instance interface { // CreateInstance ... func CreateInstance(logger types.Logger, options InstanceOptions) Instance { return &instance{ - logger: logger, - options: &options, - templates: template.CreateConfig(), - mapsTemplate: template.CreateConfig(), - mapsDir: "/etc/haproxy/maps", - metrics: options.Metrics, + logger: logger, + options: &options, + haproxyTmpl: template.CreateConfig(), + mapsTmpl: template.CreateConfig(), + modsecTmpl: template.CreateConfig(), + metrics: options.Metrics, } } type instance struct { - up bool - logger types.Logger - options *InstanceOptions - templates *template.Config - mapsTemplate *template.Config - mapsDir string - config Config - metrics types.Metrics + up bool + logger types.Logger + options *InstanceOptions + haproxyTmpl *template.Config + mapsTmpl *template.Config + modsecTmpl *template.Config + config Config + metrics types.Metrics } func (i *instance) AcmeCheck(source string) (int, error) { @@ -129,9 +132,10 @@ func (i *instance) acmeRemoveStorage(storage string) { } func (i *instance) ParseTemplates() error { - i.templates.ClearTemplates() - i.mapsTemplate.ClearTemplates() - if err := i.templates.NewTemplate( + i.haproxyTmpl.ClearTemplates() + i.mapsTmpl.ClearTemplates() + i.modsecTmpl.ClearTemplates() + if err := i.modsecTmpl.NewTemplate( "spoe-modsecurity.tmpl", "/etc/haproxy/modsecurity/spoe-modsecurity.tmpl", "/etc/haproxy/spoe-modsecurity.conf", @@ -140,7 +144,7 @@ func (i *instance) ParseTemplates() error { ); err != nil { return err } - if err := i.templates.NewTemplate( + if err := i.haproxyTmpl.NewTemplate( "haproxy.tmpl", "/etc/haproxy/template/haproxy.tmpl", "/etc/haproxy/haproxy.cfg", @@ -149,7 +153,7 @@ func (i *instance) ParseTemplates() error { ); err != nil { return err } - err := i.mapsTemplate.NewTemplate( + err := i.mapsTmpl.NewTemplate( "map.tmpl", "/etc/haproxy/maptemplate/map.tmpl", "", @@ -162,8 +166,9 @@ func (i *instance) ParseTemplates() error { func (i *instance) Config() Config { if i.config == nil { config := createConfig(options{ - mapsTemplate: i.mapsTemplate, - mapsDir: i.mapsDir, + mapsTemplate: i.mapsTmpl, + mapsDir: i.options.HAProxyMapsDir, + shardCount: i.options.BackendShards, }) i.config = config } @@ -250,7 +255,7 @@ func (i *instance) haproxyUpdate(timer *utils.Timer) { // only need to rewrtite config files if: // - !updated - there are changes that cannot be dynamically applied // - updater.cmdCnt > 0 - there are changes that was dynamically applied - err := i.templates.Write(i.config) + err := i.writeConfig() timer.Tick("write_config") if err != nil { i.logger.Error("error writing configuration: %v", err) @@ -289,6 +294,54 @@ func (i *instance) haproxyUpdate(timer *utils.Timer) { timer.Tick("reload_haproxy") } +func (i *instance) writeConfig() (err error) { + // + // modsec template execution + // + err = i.modsecTmpl.Write(i.config) + if err != nil { + return err + } + // + // haproxy template execution + // + // a single template is used to generate all haproxy cfg files + // of a multi-file configuration. `datatype` is the root type + // that the template recognizes, which will behave accordingly + // to the filled/ignored attributes. + // + type datatype struct { + Cfg Config + Global *hatypes.Global + Backends []*hatypes.Backend + } + // main cfg -- fills the .Cfg attribute + err = i.haproxyTmpl.Write(datatype{Cfg: i.config}) + if err != nil { + return err + } + // backend shards -- fills the .Global and .Backends attributes + if i.options.BackendShards > 0 { + shards := i.config.Backends().ChangedShards() + if len(shards) > 0 { + strshards := make([]string, len(shards)) + for n, j := range shards { + str := fmt.Sprintf("%03d", j) + configFile := filepath.Join(i.options.HAProxyCfgDir, "haproxy5-backend"+str+".cfg") + if err = i.haproxyTmpl.WriteOutput(datatype{ + Global: i.config.Global(), + Backends: i.config.Backends().BuildSortedShard(j), + }, configFile); err != nil { + return err + } + strshards[n] = str + } + i.logger.InfoV(2, "updated main cfg and %d backend file(s): %v", len(strshards), strshards) + } + } + return err +} + func (i *instance) updateCertExpiring() { // TODO move to dynupdate when dynamic crt update is implemented hostsAdd := i.config.Hosts().ItemsAdd() @@ -316,7 +369,7 @@ func (i *instance) check() error { i.logger.Info("(test) check was skipped") return nil } - out, err := exec.Command(i.options.HAProxyCmd, "-c", "-f", i.options.HAProxyConfigFile).CombinedOutput() + out, err := exec.Command(i.options.HAProxyCmd, "-c", "-f", i.options.HAProxyCfgDir).CombinedOutput() outstr := string(out) if err != nil { return fmt.Errorf(outstr) @@ -329,7 +382,7 @@ func (i *instance) reload() error { i.logger.Info("(test) reload was skipped") return nil } - out, err := exec.Command(i.options.ReloadCmd, i.options.ReloadStrategy, i.options.HAProxyConfigFile).CombinedOutput() + out, err := exec.Command(i.options.ReloadCmd, i.options.ReloadStrategy, i.options.HAProxyCfgDir).CombinedOutput() outstr := string(out) if len(outstr) > 0 { i.logger.Warn("output from haproxy:\n%v", outstr) diff --git a/pkg/haproxy/instance_test.go b/pkg/haproxy/instance_test.go index 56e3df9b8..dbc2dedec 100644 --- a/pkg/haproxy/instance_test.go +++ b/pkg/haproxy/instance_test.go @@ -20,6 +20,7 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "regexp" "strings" "testing" @@ -2813,6 +2814,59 @@ d1.local/ d1_app_8080 c.logger.CompareLogging(defaultLogging) } +func TestShards(t *testing.T) { + c := setupOptions(testOptions{ + t: t, + shardCount: 3, + }) + defer c.teardown() + + var h *hatypes.Host + var b *hatypes.Backend + + b = c.config.Backends().AcquireBackend("d1", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS1} + h = c.config.Hosts().AcquireHost("d1.local") + h.AddPath(b, "/") + + b = c.config.Backends().AcquireBackend("d2", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS21} + h = c.config.Hosts().AcquireHost("d2.local") + h.AddPath(b, "/") + + b = c.config.Backends().AcquireBackend("d3", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS31} + h = c.config.Hosts().AcquireHost("d3.local") + h.AddPath(b, "/") + + c.Update() + c.checkConfig(` +<> +<> +<> +<> +<> +`) + + c.checkConfigFile(` +backend d2_app_8080 + mode http + server s21 172.17.0.121:8080 weight 100 +`, "haproxy5-backend000.cfg") + + c.checkConfigFile(` +backend d1_app_8080 + mode http + server s1 172.17.0.11:8080 weight 100 +backend d3_app_8080 + mode http + server s31 172.17.0.131:8080 weight 100 +`, "haproxy5-backend002.cfg") + + c.logger.CompareLogging(` +INFO-V(2) updated main cfg and 2 backend file(s): [000 002]` + defaultLogging) +} + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * BUILDERS @@ -2820,35 +2874,45 @@ d1.local/ d1_app_8080 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ type testConfig struct { + t *testing.T + logger *helper_test.LoggerMock + instance *instance + config *config + tempdir string +} + +type testOptions struct { t *testing.T - logger *helper_test.LoggerMock - instance Instance - config Config - tempdir string - configfile string + shardCount int } func setup(t *testing.T) *testConfig { + return setupOptions(testOptions{t: t}) +} + +func setupOptions(options testOptions) *testConfig { + t := options.t logger := &helper_test.LoggerMock{T: t} tempdir, err := ioutil.TempDir("", "") if err != nil { t.Errorf("error creating tempdir: %v", err) } - configfile := tempdir + "/haproxy.cfg" instance := CreateInstance(logger, InstanceOptions{ - HAProxyConfigFile: configfile, - Metrics: helper_test.NewMetricsMock(), + HAProxyCfgDir: tempdir, + HAProxyMapsDir: tempdir, + Metrics: helper_test.NewMetricsMock(), + BackendShards: options.shardCount, }).(*instance) - if err := instance.templates.NewTemplate( + if err := instance.haproxyTmpl.NewTemplate( "haproxy.tmpl", "../../rootfs/etc/haproxy/template/haproxy.tmpl", - configfile, + filepath.Join(tempdir, "haproxy.cfg"), 0, 2048, ); err != nil { t.Errorf("error parsing haproxy.tmpl: %v", err) } - if err := instance.mapsTemplate.NewTemplate( + if err := instance.mapsTmpl.NewTemplate( "map.tmpl", "../../rootfs/etc/haproxy/maptemplate/map.tmpl", "", @@ -2857,19 +2921,14 @@ func setup(t *testing.T) *testConfig { ); err != nil { t.Errorf("error parsing map.tmpl: %v", err) } - config := createConfig(options{ - mapsTemplate: instance.mapsTemplate, - mapsDir: tempdir, - }) - instance.config = config + config := instance.Config().(*config) config.frontend.DefaultCert = "/var/haproxy/ssl/certs/default.pem" c := &testConfig{ - t: t, - logger: logger, - instance: instance, - config: config, - tempdir: tempdir, - configfile: configfile, + t: t, + logger: logger, + instance: instance, + config: config, + tempdir: tempdir, } c.configGlobal(c.config.Global()) return c @@ -2991,7 +3050,11 @@ func (c *testConfig) Update() { } func (c *testConfig) checkConfig(expected string) { - actual := strings.Replace(c.readConfig(c.configfile), c.tempdir, "/etc/haproxy/maps", -1) + c.checkConfigFile(expected, "haproxy.cfg") +} + +func (c *testConfig) checkConfigFile(expected, fileName string) { + actual := strings.Replace(c.readConfig(filepath.Join(c.tempdir, fileName)), c.tempdir, "/etc/haproxy/maps", -1) replace := map[string]string{ "<>": `global daemon @@ -3083,7 +3146,7 @@ frontend healthz break } } - c.compareText("haproxy.cfg", actual, expected) + c.compareText(fileName, actual, expected) } func (c *testConfig) checkMap(mapName, expected string) { diff --git a/pkg/haproxy/types/backends.go b/pkg/haproxy/types/backends.go index 555afdbfb..8d7d882c2 100644 --- a/pkg/haproxy/types/backends.go +++ b/pkg/haproxy/types/backends.go @@ -17,15 +17,22 @@ limitations under the License. package types import ( + "crypto/md5" "sort" ) // CreateBackends ... -func CreateBackends() *Backends { +func CreateBackends(shardCount int) *Backends { + shards := make([]map[string]*Backend, shardCount) + for i := range shards { + shards[i] = map[string]*Backend{} + } return &Backends{ - items: map[string]*Backend{}, - itemsAdd: map[string]*Backend{}, - itemsDel: map[string]*Backend{}, + items: map[string]*Backend{}, + itemsAdd: map[string]*Backend{}, + itemsDel: map[string]*Backend{}, + shards: shards, + changedShards: map[int]bool{}, } } @@ -48,6 +55,7 @@ func (b *Backends) ItemsDel() map[string]*Backend { func (b *Backends) Commit() { b.itemsAdd = map[string]*Backend{} b.itemsDel = map[string]*Backend{} + b.changedShards = map[int]bool{} } // Changed ... @@ -55,11 +63,38 @@ func (b *Backends) Changed() bool { return len(b.itemsAdd) > 0 || len(b.itemsDel) > 0 } +// ChangedShards ... +func (b *Backends) ChangedShards() []int { + changed := []int{} + for i, c := range b.changedShards { + if c { + changed = append(changed, i) + } + } + sort.Ints(changed) + return changed +} + // BuildSortedItems ... func (b *Backends) BuildSortedItems() []*Backend { - items := make([]*Backend, len(b.items)) + // TODO BuildSortedItems() is currently used only by the backend template. + // The main cfg template doesn't care if there are backend shards or not, + // so the logic is here, but this doesn't seem to be a good place. + if len(b.shards) == 0 { + return b.buildSortedItems(b.items) + } + return nil +} + +// BuildSortedShard ... +func (b *Backends) BuildSortedShard(shardRef int) []*Backend { + return b.buildSortedItems(b.shards[shardRef]) +} + +func (b *Backends) buildSortedItems(backendItems map[string]*Backend) []*Backend { + items := make([]*Backend, len(backendItems)) var i int - for _, item := range b.items { + for _, item := range backendItems { items[i] = item i++ } @@ -80,9 +115,14 @@ func (b *Backends) AcquireBackend(namespace, name, port string) *Backend { if backend := b.FindBackend(namespace, name, port); backend != nil { return backend } - backend := createBackend(namespace, name, port) + shardCount := len(b.shards) + backend := createBackend(shardCount, namespace, name, port) b.items[backend.ID] = backend b.itemsAdd[backend.ID] = backend + if shardCount > 0 { + b.shards[backend.shard][backend.ID] = backend + } + b.changedShards[backend.shard] = true return backend } @@ -101,6 +141,10 @@ func (b *Backends) RemoveAll(backendID []BackendID) { for _, backend := range backendID { id := backend.String() if item, found := b.items[id]; found { + if len(b.shards) > 0 { + delete(b.shards[item.shard], id) + } + b.changedShards[item.shard] = true b.itemsDel[id] = item delete(b.items, id) } @@ -131,9 +175,32 @@ func (b BackendID) String() string { return b.id } -func createBackend(namespace, name, port string) *Backend { +func createBackend(shards int, namespace, name, port string) *Backend { + id := buildID(namespace, name, port) + var shard int + if shards > 0 { + hash := md5.Sum([]byte(id)) + part0 := uint64(hash[0])<<56 | + uint64(hash[1])<<48 | + uint64(hash[2])<<40 | + uint64(hash[3])<<32 | + uint64(hash[4])<<24 | + uint64(hash[5])<<16 | + uint64(hash[6])<<8 | + uint64(hash[7]) + part1 := uint64(hash[8])<<56 | + uint64(hash[9])<<48 | + uint64(hash[10])<<40 | + uint64(hash[11])<<32 | + uint64(hash[12])<<24 | + uint64(hash[13])<<16 | + uint64(hash[14])<<8 | + uint64(hash[15]) + shard = int(uint64(part0^part1) % uint64(shards)) + } return &Backend{ - ID: buildID(namespace, name, port), + shard: shard, + ID: id, Namespace: namespace, Name: name, Port: port, diff --git a/pkg/haproxy/types/backends_test.go b/pkg/haproxy/types/backends_test.go index 2de6c5303..a1380531a 100644 --- a/pkg/haproxy/types/backends_test.go +++ b/pkg/haproxy/types/backends_test.go @@ -18,9 +18,116 @@ package types import ( "fmt" + "sort" + "strings" "testing" ) +func TestBackendCrud(t *testing.T) { + testCases := []struct { + shardCnt int + add []string + del []string + expected []string + expAdd []string + expDel []string + expShards [][]string + }{ + // 0 + {}, + // 1 + { + add: []string{"default_app_8080"}, + expected: []string{"default_app_8080"}, + expAdd: []string{"default_app_8080"}, + }, + // 2 + { + add: []string{"default_app_8080"}, + del: []string{"default_app_8080"}, + expAdd: []string{"default_app_8080"}, + expDel: []string{"default_app_8080"}, + }, + // 3 + { + add: []string{"default_app1_8080", "default_app2_8080"}, + del: []string{"default_app1_8080"}, + expected: []string{"default_app2_8080"}, + expAdd: []string{"default_app1_8080", "default_app2_8080"}, + expDel: []string{"default_app1_8080"}, + }, + // 4 + { + shardCnt: 3, + add: []string{"default_app1_8080", "default_app2_8080", "default_app3_8080", "default_app4_8080"}, + expected: []string{"default_app1_8080", "default_app2_8080", "default_app3_8080", "default_app4_8080"}, + expAdd: []string{"default_app1_8080", "default_app2_8080", "default_app3_8080", "default_app4_8080"}, + expShards: [][]string{ + {"default_app2_8080"}, + {"default_app1_8080", "default_app4_8080"}, + {"default_app3_8080"}, + }, + }, + // 5 + { + shardCnt: 3, + add: []string{"default_app1_8080", "default_app2_8080", "default_app3_8080", "default_app4_8080"}, + del: []string{"default_app1_8080", "default_app2_8080"}, + expected: []string{"default_app3_8080", "default_app4_8080"}, + expAdd: []string{"default_app1_8080", "default_app2_8080", "default_app3_8080", "default_app4_8080"}, + expDel: []string{"default_app1_8080", "default_app2_8080"}, + expShards: [][]string{ + {}, + {"default_app4_8080"}, + {"default_app3_8080"}, + }, + }, + } + toarray := func(items map[string]*Backend) []string { + if len(items) == 0 { + return nil + } + result := make([]string, len(items)) + var i int + for item := range items { + result[i] = item + i++ + } + sort.Strings(result) + return result + } + for i, test := range testCases { + c := setup(t) + backends := CreateBackends(test.shardCnt) + for _, add := range test.add { + p := strings.Split(add, "_") + backends.AcquireBackend(p[0], p[1], p[2]) + } + var backendIDs []BackendID + for _, del := range test.del { + p := strings.Split(del, "_") + if b := backends.FindBackend(p[0], p[1], p[2]); b != nil { + backendIDs = append(backendIDs, b.BackendID()) + } + } + backends.RemoveAll(backendIDs) + c.compareObjects("items", i, toarray(backends.items), test.expected) + c.compareObjects("itemsAdd", i, toarray(backends.itemsAdd), test.expAdd) + c.compareObjects("itemsDel", i, toarray(backends.itemsDel), test.expDel) + var shards [][]string + for _, shard := range backends.shards { + names := []string{} + for name := range shard { + names = append(names, name) + } + sort.Strings(names) + shards = append(shards, names) + } + c.compareObjects("shards", i, shards, test.expShards) + c.teardown() + } +} + func TestBuildID(t *testing.T) { testCases := []struct { namespace string diff --git a/pkg/haproxy/types/global_test.go b/pkg/haproxy/types/global_test.go index 82ca65067..4b153cbb3 100644 --- a/pkg/haproxy/types/global_test.go +++ b/pkg/haproxy/types/global_test.go @@ -146,3 +146,21 @@ func TestShrink(t *testing.T) { } } } + +type testConfig struct { + t *testing.T +} + +func setup(t *testing.T) *testConfig { + return &testConfig{ + t: t, + } +} + +func (c *testConfig) teardown() {} + +func (c *testConfig) compareObjects(name string, index int, actual, expected interface{}) { + if !reflect.DeepEqual(actual, expected) { + c.t.Errorf("%s on %d differs - expected: %v - actual: %v", name, index, expected, actual) + } +} diff --git a/pkg/haproxy/types/types.go b/pkg/haproxy/types/types.go index 3d46ae057..ff62ee9d4 100644 --- a/pkg/haproxy/types/types.go +++ b/pkg/haproxy/types/types.go @@ -381,6 +381,8 @@ type Backends struct { items, itemsAdd, itemsDel map[string]*Backend // defaultBackend *Backend + shards []map[string]*Backend + changedShards map[int]bool } // BackendID ... @@ -398,6 +400,7 @@ type Backend struct { // // IMPLEMENT // use BackendID + shard int ID string Namespace string Name string diff --git a/rootfs/etc/haproxy/template/haproxy.tmpl b/rootfs/etc/haproxy/template/haproxy.tmpl index c644dc877..2c7e832ff 100644 --- a/rootfs/etc/haproxy/template/haproxy.tmpl +++ b/rootfs/etc/haproxy/template/haproxy.tmpl @@ -6,12 +6,58 @@ # # This file is automatically updated, do not edit # # # -{{- $cfg := . }} -{{- $global := $cfg.Global }} -{{- $backends := $cfg.Backends }} -{{- $frontend := $cfg.Frontend }} -{{- $fmaps := $frontend.Maps }} -{{- $hosts := $cfg.Hosts }} + +{{- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * TEMPLATE DECLARATIONS + * + * 1. main cfg + * 2. backend shards + * + */}} + +{{- if .Cfg }} + {{- $cfg := .Cfg }} + {{- $global := $cfg.Global }} + {{- $userlists := $cfg.Userlists.BuildSortedItems }} + {{- $tcpbackends := $cfg.TCPBackends.BuildSortedItems}} + {{- $backends := $cfg.Backends }} + {{- $backendItems := $backends.BuildSortedItems }} + {{- $frontend := $cfg.Frontend }} + {{- $fmaps := $frontend.Maps }} + {{- $hosts := $cfg.Hosts }} + {{- template "global" map $global }} + {{- if $global.DNS.Resolvers }} + {{- template "dnresolvers" map $global.DNS.Resolvers }} + {{- end }} + {{- if $userlists }} + {{- template "userlists" map $userlists }} + {{- end }} + {{- if $tcpbackends }} + {{- template "tcpbackends" map $global $tcpbackends }} + {{- end }} + {{- if $backendItems }} + {{- template "backends" map $global $backendItems true }} + {{- end }} + {{- template "backend-support" map $global $backends }} + {{- if $fmaps }} + {{- template "frontends" map $global $frontend $hosts $fmaps $backends.DefaultBackend }} + {{- end }} + {{- template "frontend-support" map $global }} +{{- else if and .Global .Backends }} + {{- $global := .Global }} + {{- $backendItems := .Backends }} + {{- template "backends" map $global $backendItems false }} +{{- end }} + +{{- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * TEMPLATE DEFINITIONS + * + */}} + +{{- define "global" }} +{{- $global := .p1 }} global daemon {{- if $global.UseHAProxyUser }} @@ -124,14 +170,17 @@ defaults {{- range $snippet := $global.CustomDefaults }} {{ $snippet }} {{- end }} +{{- end }}{{/* define "global" */}} -{{- if $global.DNS.Resolvers }} + +{{- define "dnresolvers" }} +{{- $resolvers := .p1 }} # # # # # # # # # # # # # # # # # # # # # # DNS RESOLVERS # -{{- range $resolver := $global.DNS.Resolvers }} +{{- range $resolver := $resolvers }} resolvers {{ $resolver.Name }} {{- range $ns := $resolver.Nameservers }} nameserver {{ $ns.Name }} {{ $ns.Endpoint }} @@ -141,10 +190,11 @@ resolvers {{ $resolver.Name }} hold valid {{ $resolver.HoldValid }} timeout retry {{ $resolver.TimeoutRetry }} {{- end }} -{{- end }} +{{- end }}{{/* define "dnresolvers" */}} -{{- $userlists := $cfg.Userlists.BuildSortedItems }} -{{- if $userlists }} + +{{- define "userlists" }} +{{- $userlists := .p1 }} # # # # # # # # # # # # # # # # # # # # # @@ -156,10 +206,12 @@ userlist {{ $userlist.Name }} user {{ $user.Name }} {{ if not $user.Encrypted }}insecure-{{ end }}password {{ $user.Passwd }} {{- end }} {{- end }} -{{- end }} +{{- end }}{{/* define "userlists" */}} + -{{- $tcpbackends := $cfg.TCPBackends.BuildSortedItems}} -{{- if $tcpbackends }} +{{- define "tcpbackends" }} +{{- $global := .p1 }} +{{- $tcpbackends := .p2 }} # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # @@ -203,11 +255,14 @@ listen _tcp_{{ $backend.Name }}_{{ $backend.Port }} {{- end }} {{- end }}{{/* range TCPBackends */}} -{{- end }}{{/* if has TCPBackend */}} +{{- end }}{{/* define "tcpbackends" */}} -{{- $backendItems := $backends.BuildSortedItems }} -{{- if $backendItems }} +{{- define "backends" }} +{{- $global := .p1 }} +{{- $backendItems := .p2 }} +{{- $shared := .p3 }} +{{- if $shared }} # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # @@ -215,6 +270,7 @@ listen _tcp_{{ $backend.Name }}_{{ $backend.Port }} # # BACKENDS # # # +{{- end }} {{- range $backend := $backendItems }} backend {{ $backend.ID }} mode {{ if $backend.ModeTCP }}tcp{{ else }}http{{ end }} @@ -555,7 +611,7 @@ backend {{ $backend.ID }} {{- end }} {{- end }} -{{- end }}{{/* if backendItems */}} +{{- end }}{{/* define "backends" */}} {{- define "backend" }} {{- $backend := .p1 }} @@ -592,6 +648,11 @@ backend {{ $backend.ID }} {{- end }} {{- end }} + +{{- define "backend-support" }} +{{- $global := .p1 }} +{{- $backends := .p2 }} + {{- if $global.Acme.Enabled }} # # # # # # # # # # # # # # # # # # # @@ -614,7 +675,15 @@ backend _error404 http-request use-service lua.send-404 {{- end }} -{{- if $fmaps }} +{{- end }}{{/* define "backend-support" */}} + + +{{- define "frontends" }} +{{- $global := .p1 }} +{{- $frontend := .p2 }} +{{- $hosts := .p3 }} +{{- $fmaps := .p4 }} +{{- $defaultbackend := .p5 }} # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # @@ -828,7 +897,7 @@ frontend _front_http use_backend _acme_challenge if acme-challenge {{- end }} -{{- template "defaultbackend" map $cfg }} +{{- template "defaultbackend" map $hosts $defaultbackend }} # # # # # # # # # # # # # # # # # # # # # @@ -1003,29 +1072,31 @@ frontend {{ $frontend.Name }} use_backend %[var(req.snibackend)] {{- "" }} if { var(req.snibackend) -m found } {{- end }} -{{- template "defaultbackend" map $cfg }} +{{- template "defaultbackend" map $hosts $defaultbackend }} -{{- end }}{{/* if $fmaps */}} +{{- end }}{{/* define "frontends" */}} {{- /*------------------------------------*/}} {{- /*------------------------------------*/}} {{- define "defaultbackend" }} -{{- $cfg := .p1 }} -{{- $backends := $cfg.Backends }} -{{- $hosts := $cfg.Hosts }} +{{- $hosts := .p1 }} +{{- $defaultbackend := .p2 }} {{- if $hosts.DefaultHost }} {{- range $path := $hosts.DefaultHost.Paths }} use_backend {{ $path.Backend.ID }} {{- if ne $path.Path "/" }} if { path_beg {{ $path.Path }} }{{ end }} {{- end }} {{- end }} -{{- if $backends.DefaultBackend }} - default_backend {{ $backends.DefaultBackend.ID }} +{{- if $defaultbackend }} + default_backend {{ $defaultbackend.ID }} {{- else }} default_backend _error404 {{- end }} {{- end }} +{{- define "frontend-support" }} +{{- $global := .p1 }} + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # @@ -1093,5 +1164,6 @@ backend spoe-modsecurity {{- range $i, $endpoint := $global.ModSecurity.Endpoints }} server modsec-spoa{{ $i }} {{ $endpoint }} {{- end }} - {{- end }} + +{{- end }}{{/* define "frontend-support" */}}