diff --git a/pkg/annotations/cfgSnippetHandler.go b/pkg/annotations/cfgSnippetHandler.go index d646ab1b..447af25d 100644 --- a/pkg/annotations/cfgSnippetHandler.go +++ b/pkg/annotations/cfgSnippetHandler.go @@ -14,17 +14,14 @@ type ConfigSnippetHandler struct{} func (h ConfigSnippetHandler) Update(k store.K8s, api haproxy.HAProxy, ann Annotations) (err error) { // We get the configmap configsnippet value - configmapCfgSnippetValue, errConfigmapCfgSnippet := getConfigmapConfigSnippet(k.BackendsWithNoConfigSnippets, api) - if errConfigmapCfgSnippet != nil { - return errConfigmapCfgSnippet - } + configmapCfgSnippetValue := getConfigmapConfigSnippet(k.BackendsWithNoConfigSnippets, api) // We pass the configmap config snippet value to be inserted at top of the comment section for every config snippet section return updateConfigSnippet(api, configmapCfgSnippetValue) } -func getConfigmapConfigSnippet(backendsWithNoConfigSnippets map[string]struct{}, api api.HAProxyClient) (configmapCfgSnippetValue []string, err error) { +func getConfigmapConfigSnippet(backendsWithNoConfigSnippets map[string]struct{}, api api.HAProxyClient) []string { // configmap config snippet if any. - configmapCfgSnippetValue = []string{} + configmapCfgSnippetValue := []string{} // configmap config snippet will be hold in special backend 'configmap' with origin 'configmap' if configmapCfgSnippet := cfgSnippet.backends["configmap"]["configmap"]; configmapCfgSnippet != nil && configmapCfgSnippet.status != store.DELETED && @@ -32,22 +29,18 @@ func getConfigmapConfigSnippet(backendsWithNoConfigSnippets map[string]struct{}, configmapCfgSnippetValue = configmapCfgSnippet.value // if any existing and enabled configmap configsnippet then add a special insertion for every existing backend // to replicate everywhere the configmap insertion. - if backends, errGet := api.BackendsGet(); errGet == nil { - for _, backend := range backends { - if _, ok := backendsWithNoConfigSnippets[backend.Name]; ok { - continue - } - if _, ok := cfgSnippet.backends[backend.Name]; !ok { - cfgSnippet.backends[backend.Name] = map[string]*cfgData{ - "configmap-insertion": {status: store.ADDED}, - } + for _, backend := range api.BackendsGet() { + if _, ok := backendsWithNoConfigSnippets[backend.Name]; ok { + continue + } + if _, ok := cfgSnippet.backends[backend.Name]; !ok { + cfgSnippet.backends[backend.Name] = map[string]*cfgData{ + "configmap-insertion": {status: store.ADDED}, } } - } else { - err = errGet } } - return + return configmapCfgSnippetValue } func updateConfigSnippet(api api.HAProxyClient, configmapCfgSnippetValue []string) (err error) { diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 6e84ba9b..fe6dc460 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -181,7 +181,7 @@ func (c *HAProxyController) updateHAProxy() { c.setToReady() } - err = c.haproxy.APICommitTransaction() + err = c.haproxy.APIFinalCommitTransaction() if err != nil { logger.Error("unable to Sync HAProxy configuration !!") logger.Error(err) @@ -196,6 +196,8 @@ func (c *HAProxyController) updateHAProxy() { c.updateHAProxy() return } + // If any error not from config snippet then pop the previous state of backends + logger.Error(c.haproxy.PopPreviousBackends()) return } @@ -213,7 +215,8 @@ func (c *HAProxyController) updateHAProxy() { } c.clean(false) - + // If transaction succeeds thenpush backends state for any future recover. + logger.Error(c.haproxy.PushPreviousBackends()) logger.Trace("HAProxy config sync ended") } diff --git a/pkg/gateways/gateways.go b/pkg/gateways/gateways.go index 23e565a1..f199c662 100644 --- a/pkg/gateways/gateways.go +++ b/pkg/gateways/gateways.go @@ -203,16 +203,13 @@ func (gm GatewayManagerImpl) manageTCPRoutes() { } // If not called on the route, the afferent backend will be automatically deleted. - errBackendCreate := gm.haproxyClient.BackendCreateIfNotExist( + gm.haproxyClient.BackendCreateIfNotExist( models.Backend{ Name: getBackendName(*tcproute), Mode: "tcp", DefaultServer: &models.DefaultServer{ServerParams: models.ServerParams{Check: "enabled"}}, }) - if errBackendCreate != nil { - logger.Error(errBackendCreate) - continue - } + _, backendExists := gm.backends[tcpRouteBackendName] instance.ReloadIf(!backendExists, "modification in backend for tcproute '%s/%s'", tcproute.Namespace, tcproute.Name) gm.backends[tcpRouteBackendName] = struct{}{} @@ -369,7 +366,7 @@ func (gm GatewayManagerImpl) isNamespaceGranted(namespace string, backendRef sto // addServersToRoute adds all the servers from the backendrefs from tcproute according validation rules. func (gm GatewayManagerImpl) addServersToRoute(route store.TCPRoute) (reload bool, err error) { backendName := getBackendName(route) - gm.haproxyClient.BackendServerDeleteAll(backendName) + _ = gm.haproxyClient.BackendServerDeleteAll(backendName) i := 0 var servers []string defer func() { diff --git a/pkg/handler/https.go b/pkg/handler/https.go index 574c43f6..856bb631 100644 --- a/pkg/handler/https.go +++ b/pkg/handler/https.go @@ -187,9 +187,8 @@ func (handler HTTPS) Update(k store.K8s, h haproxy.HAProxy, a annotations.Annota } // ssl-passthrough _, errFtSSL := h.FrontendGet(h.FrontSSL) - _, errBdSSL := h.BackendGet(h.BackSSL) if haproxy.SSLPassthrough { - if errFtSSL != nil || errBdSSL != nil { + if errFtSSL != nil || !h.BackendExists(h.BackSSL) { logger.Error(handler.enableSSLPassthrough(h)) instance.Reload("SSLPassthrough enabled") } @@ -223,13 +222,13 @@ func (handler HTTPS) enableSSLPassthrough(h haproxy.HAProxy) (err error) { } // Create backend for proxy chaining (chaining // ssl-passthrough frontend to ssl-offload backend) + h.BackendCreatePermanently(models.Backend{ + Name: h.BackSSL, + Mode: "tcp", + }) var errors utils.Errors errors.Add( - h.BackendCreate(models.Backend{ - Name: h.BackSSL, - Mode: "tcp", - }), - h.BackendServerCreate(h.BackSSL, models.Server{ + h.BackendServerCreateOrUpdate(h.BackSSL, models.Server{ Name: h.FrontHTTPS, Address: "unix@" + handler.unixSocketPath(h), ServerParams: models.ServerParams{SendProxyV2: "enabled"}, @@ -248,10 +247,7 @@ func (handler HTTPS) disableSSLPassthrough(h haproxy.HAProxy) (err error) { return err } h.DeleteFTRules(h.FrontSSL) - err = h.BackendDelete(h.BackSSL) - if err != nil { - return err - } + h.BackendDelete(h.BackSSL) if err = handler.toggleSSLPassthrough(false, h); err != nil { return err } diff --git a/pkg/handler/pprof.go b/pkg/handler/pprof.go index 07a6550b..4009743c 100644 --- a/pkg/handler/pprof.go +++ b/pkg/handler/pprof.go @@ -31,16 +31,14 @@ type Pprof struct{} func (handler Pprof) Update(k store.K8s, h haproxy.HAProxy, a annotations.Annotations) (err error) { k.BackendsWithNoConfigSnippets[pprofBackend] = struct{}{} - _, err = h.BackendGet(pprofBackend) - if err != nil { - err = h.BackendCreatePermanently(models.Backend{ + + if !h.BackendExists(pprofBackend) { + h.BackendCreatePermanently(models.Backend{ Name: pprofBackend, Mode: "http", }) - if err != nil { - return - } - err = h.BackendServerCreate(pprofBackend, models.Server{ + + err = h.BackendServerCreateOrUpdate(pprofBackend, models.Server{ Name: pprofBackend, Address: fmt.Sprintf("127.0.0.1:%d", h.Env.ControllerPort), }) @@ -49,6 +47,7 @@ func (handler Pprof) Update(k store.K8s, h haproxy.HAProxy, a annotations.Annota } logger.Debug("pprof backend created") } + err = route.AddHostPathRoute(route.Route{ BackendName: pprofBackend, Path: &store.IngressPath{ @@ -59,6 +58,6 @@ func (handler Pprof) Update(k store.K8s, h haproxy.HAProxy, a annotations.Annota if err != nil { return } - // instance.Reload("pprof backend created") + return } diff --git a/pkg/handler/prometheus.go b/pkg/handler/prometheus.go index 0d2eed88..028f08f4 100644 --- a/pkg/handler/prometheus.go +++ b/pkg/handler/prometheus.go @@ -31,8 +31,7 @@ func (handler PrometheusEndpoint) Update(k store.K8s, h haproxy.HAProxy, a annot status := store.EMPTY var secret *store.Secret - _, errBackend := h.BackendGet(prometheusBackendName) - backendExists := errBackend == nil + backendExists := h.BackendExists(prometheusBackendName) annSecret := annotations.String("prometheus-endpoint-auth-secret", k.ConfigMaps.Main.Annotations) var secretExists, secretChanged, userListChanged bool diff --git a/pkg/handler/refresh.go b/pkg/handler/refresh.go index cecbb057..f0ba3610 100644 --- a/pkg/handler/refresh.go +++ b/pkg/handler/refresh.go @@ -17,7 +17,6 @@ package handler import ( "github.com/haproxytech/kubernetes-ingress/pkg/annotations" "github.com/haproxytech/kubernetes-ingress/pkg/haproxy" - "github.com/haproxytech/kubernetes-ingress/pkg/haproxy/instance" "github.com/haproxytech/kubernetes-ingress/pkg/store" ) @@ -38,13 +37,6 @@ func (handler Refresh) Update(k store.K8s, h haproxy.HAProxy, a annotations.Anno h.RefreshRules(h.HAProxyClient) // Maps h.RefreshMaps(h.HAProxyClient) - // Backends - deleted, err := h.RefreshBackends() - instance.ReloadIf(len(deleted) > 0, "some backends are deleted") - logger.Error(err) - for _, backend := range deleted { - logger.Debugf("Backend '%s' deleted", backend) - annotations.RemoveBackendCfgSnippet(backend) - } + return } diff --git a/pkg/haproxy/api/acl.go b/pkg/haproxy/api/acl.go index 4509595d..8ad30bdc 100644 --- a/pkg/haproxy/api/acl.go +++ b/pkg/haproxy/api/acl.go @@ -1,12 +1,25 @@ package api -import "github.com/haproxytech/client-native/v5/models" +import ( + "fmt" + + "github.com/haproxytech/client-native/v5/models" +) func (c *clientNative) ACLsGet(parentType, parentName string, aclName ...string) (models.Acls, error) { configuration, err := c.nativeAPI.Configuration() if err != nil { return nil, err } + + if parentType == "backend" { + backend, exists := c.backends[parentName] + if !exists { + return nil, fmt.Errorf("can't get acls for unexisting backend %s : %w", parentName, ErrNotFound) + } + return backend.ACLList, nil + } + _, acls, err := configuration.GetACLs(parentType, parentName, c.activeTransaction, aclName...) if err != nil { return nil, err @@ -39,6 +52,17 @@ func (c *clientNative) ACLDeleteAll(parentType string, parentName string) error if err != nil { return err } + + if parentType == "backend" { + backend, exists := c.backends[parentName] + if !exists { + return fmt.Errorf("can't delete acls for unexisting backend %s : %w", parentName, ErrNotFound) + } + backend.ACLList = nil + c.backends[parentName] = backend + return nil + } + _, acls, errGet := configuration.GetACLs(parentType, parentName, c.activeTransaction) if errGet != nil { return errGet @@ -58,6 +82,15 @@ func (c *clientNative) ACLCreate(parentType string, parentName string, data *mod if err != nil { return err } + if parentType == "backend" { + backend, exists := c.backends[parentName] + if !exists { + return fmt.Errorf("can't create acl for unexisting backend %s : %w", parentName, ErrNotFound) + } + backend.ACLList = append(backend.ACLList, data) + c.backends[parentName] = backend + return nil + } return configuration.CreateACL(parentType, parentName, data, c.activeTransaction, 0) } diff --git a/pkg/haproxy/api/api.go b/pkg/haproxy/api/api.go index ad5541dd..63ae4888 100644 --- a/pkg/haproxy/api/api.go +++ b/pkg/haproxy/api/api.go @@ -2,8 +2,10 @@ package api import ( "context" + "encoding/json" clientnative "github.com/haproxytech/client-native/v5" + "github.com/haproxytech/client-native/v5/config-parser/types" "github.com/haproxytech/client-native/v5/configuration" cfgoptions "github.com/haproxytech/client-native/v5/configuration/options" "github.com/haproxytech/client-native/v5/models" @@ -11,6 +13,7 @@ import ( "github.com/haproxytech/client-native/v5/runtime" runtimeoptions "github.com/haproxytech/client-native/v5/runtime/options" + "github.com/haproxytech/kubernetes-ingress/pkg/haproxy/instance" "github.com/haproxytech/kubernetes-ingress/pkg/store" "github.com/haproxytech/kubernetes-ingress/pkg/utils" ) @@ -19,9 +22,12 @@ import ( // Map payload or socket data cannot be bigger than tune.bufsize const BufferSize = 16000 +var logger = utils.GetLogger() + type HAProxyClient interface { //nolint:interfacebloat APIStartTransaction() error APICommitTransaction() error + APIFinalCommitTransaction() error APIDisposeTransaction() ACLsGet(parentType, parentName string, aclName ...string) (models.Acls, error) ACLGet(id int64, parentType, parentName string) (*models.ACL, error) @@ -29,22 +35,24 @@ type HAProxyClient interface { //nolint:interfacebloat ACLDeleteAll(parentType string, parentName string) error ACLCreate(parentType string, parentName string, data *models.ACL) error ACLEdit(id int64, parentType string, parentName string, data *models.ACL) error - BackendsGet() (models.Backends, error) + BackendsGet() models.Backends BackendGet(backendName string) (*models.Backend, error) - BackendCreate(backend models.Backend) error - BackendCreatePermanently(backend models.Backend) error - BackendCreateIfNotExist(backend models.Backend) error - BackendEdit(backend models.Backend) error - BackendDelete(backendName string) error + BackendExists(backendName string) bool + BackendCreatePermanently(backend models.Backend) + BackendCreateIfNotExist(backend models.Backend) + BackendCreateOrUpdate(backend models.Backend) (map[string][]interface{}, bool) + BackendDelete(backendName string) + BackendDeleteAllUnnecessary() ([]string, error) BackendCfgSnippetSet(backendName string, value []string) error BackendHTTPRequestRuleCreate(backend string, rule models.HTTPRequestRule) error BackendRuleDeleteAll(backend string) - BackendServerDeleteAll(backendName string) (deleteServers bool) + BackendServerDeleteAll(backendName string) error BackendServerCreate(backendName string, data models.Server) error BackendServerEdit(backendName string, data models.Server) error - BackendServerCreateOrEdit(backendName string, data models.Server) error BackendServerDelete(backendName string, serverName string) error + BackendServerGet(serverName, backendNa string) (*models.Server, error) BackendServersGet(backendName string) (models.Servers, error) + BackendServerCreateOrUpdate(backendName string, data models.Server) error BackendSwitchingRulesGet(frontendName string) (models.BackendSwitchingRules, error) BackendSwitchingRuleCreate(frontend string, rule models.BackendSwitchingRule) error CaptureCreate(frontend string, rule models.Capture) error @@ -89,15 +97,15 @@ type HAProxyClient interface { //nolint:interfacebloat LogTargetCreate(parentType, parentName string, rule models.LogTarget) error LogTargetsGet(parentType, parentName string) (models.LogTargets, error) LogTargetDeleteAll(parentType, parentName string) (err error) + PushPreviousBackends() error + PopPreviousBackends() error TCPRequestRuleCreate(parentType, parentName string, rule models.TCPRequestRule) error TCPRequestRulesGet(parentType, parentName string) (models.TCPRequestRules, error) TCPRequestRuleDeleteAll(parentType, parentName string) (err error) PeerEntryEdit(peerSection string, peer models.PeerEntry) error PeerEntryCreateOrEdit(peerSection string, peer models.PeerEntry) error - RefreshBackends() (deleted []string, err error) SetMapContent(mapFile string, payload []string) error SetServerAddrAndState([]RuntimeServerData) error - ServerGet(serverName, backendNa string) (models.Server, error) SetAuxCfgFile(auxCfgFile string) SyncBackendSrvs(backend *store.RuntimeBackend, portUpdated bool) error UserListDeleteAll() error @@ -110,12 +118,22 @@ type HAProxyClient interface { //nolint:interfacebloat CrtListEntryAdd(crtList string, entry runtime.CrtListEntry) error } +type Backend struct { // use same names as in client native v6 + BackendBase models.Backend `json:",inline"` + Servers map[string]models.Server + ACLList models.Acls `json:"acl_list,omitempty"` + HTTPRequestsRules models.HTTPRequestRules `json:"http_request_rule_list,omitempty"` + ConfigSnippets []string + Permanent bool + Used bool +} + type clientNative struct { nativeAPI clientnative.HAProxyClient - activeBackends map[string]struct{} - permanentBackends map[string]struct{} activeTransaction string activeTransactionHasChanges bool + backends map[string]Backend + previousBackends []byte } func New(transactionDir, configFile, programPath, runtimeSocket string) (client HAProxyClient, err error) { //nolint:ireturn @@ -149,9 +167,8 @@ func New(transactionDir, configFile, programPath, runtimeSocket string) (client } cn := clientNative{ - nativeAPI: cnHAProxyClient, - activeBackends: make(map[string]struct{}), - permanentBackends: make(map[string]struct{}), + nativeAPI: cnHAProxyClient, + backends: make(map[string]Backend), } return &cn, nil } @@ -170,7 +187,7 @@ func (c *clientNative) APIStartTransaction() error { if err != nil { return err } - utils.GetLogger().WithField(utils.LogFieldTransactionID, transaction.ID) + logger.WithField(utils.LogFieldTransactionID, transaction.ID) c.activeTransaction = transaction.ID c.activeTransactionHasChanges = false return nil @@ -191,8 +208,42 @@ func (c *clientNative) APICommitTransaction() error { return err } +func (c *clientNative) APIFinalCommitTransaction() error { + configuration, err := c.nativeAPI.Configuration() + if err != nil { + return err + } + + var errs utils.Errors + // First we remove all backends ... + deletedBackends, _ := c.BackendDeleteAllUnnecessary() + for _, deletedBackend := range deletedBackends { + instance.Reload("backend '%s' deleted", deletedBackend) + } + // ... then we parse the backends to take decisions. + for backendName, backend := range c.backends { + errs.Add(c.processBackend(&backend.BackendBase, configuration)) + errs.AddErrors(c.processServers(backendName, configuration)) + errs.Add(c.processConfigSnippets(backendName, backend.ConfigSnippets, configuration)) + errs.AddErrors(c.processACLs(backendName, backend.ACLList, configuration)) + errs.AddErrors(c.processHTTPRequestRules(backendName, backend.HTTPRequestsRules, configuration)) + backend.Used = false + c.backends[backendName] = backend + } + + if !c.activeTransactionHasChanges { + if errDel := configuration.DeleteTransaction(c.activeTransaction); errDel != nil { + errs.Add(errDel) + } + return errs.Result() + } + _, err = configuration.CommitTransaction(c.activeTransaction) + logger.Error(errs.Result()) + return err +} + func (c *clientNative) APIDisposeTransaction() { - utils.GetLogger().ResetFields() + logger.ResetFields() c.activeTransaction = "" c.activeTransactionHasChanges = false } @@ -200,7 +251,7 @@ func (c *clientNative) APIDisposeTransaction() { func (c *clientNative) SetAuxCfgFile(auxCfgFile string) { configuration, err := c.nativeAPI.Configuration() if err != nil { - logger := utils.GetLogger() + logger := logger logger.Error(err) } if auxCfgFile == "" { @@ -209,3 +260,92 @@ func (c *clientNative) SetAuxCfgFile(auxCfgFile string) { } configuration.SetValidateConfigFiles(nil, []string{auxCfgFile}) } + +func (c *clientNative) processBackend(backend *models.Backend, configuration configuration.Configuration) error { + // Try to create the backend ... + errCreateBackend := configuration.CreateBackend(backend, c.activeTransaction, 0) + if errCreateBackend != nil { + // ... maybe it's already existing, so just edit it. + return configuration.EditBackend(backend.Name, backend, c.activeTransaction, 0) + } + return nil +} + +func (c *clientNative) processServers(backendName string, configuration configuration.Configuration) utils.Errors { + var errs utils.Errors + // Same for servers. + servers, _ := c.BackendServersGet(backendName) + for _, server := range servers { + errCreateServer := configuration.CreateServer("backend", backendName, server, c.activeTransaction, 0) + if errCreateServer != nil { + errs.Add(configuration.EditServer(server.Name, "backend", backendName, server, c.activeTransaction, 0)) + } + } + return errs +} + +func (c *clientNative) processConfigSnippets(backendName string, configSnippets []string, configuration configuration.Configuration) error { + // Same for backend configsnippets. + config, err := configuration.GetParser(c.activeTransaction) + if err != nil { + return err + } + if len(configSnippets) > 0 { + return config.Set("backend", backendName, "config-snippet", types.StringSliceC{Value: configSnippets}) + } else { + return config.Set("backend", backendName, "config-snippet", nil) + } +} + +func (c *clientNative) processACLs(backendName string, aclsList models.Acls, configuration configuration.Configuration) utils.Errors { + // we remove all acls because of permanent backend still in parsers. + _, existingACLs, _ := configuration.GetACLs("backend", backendName, c.activeTransaction) + for range existingACLs { + _ = configuration.DeleteACL(0, "backend", backendName, c.activeTransaction, 0) + } + var errs utils.Errors + // we (re)create all acls + for _, acl := range aclsList { + errs.Add(configuration.CreateACL("backend", backendName, acl, c.activeTransaction, 0)) + } + return errs +} + +func (c *clientNative) processHTTPRequestRules(backendName string, httpRequestsRules models.HTTPRequestRules, configuration configuration.Configuration) utils.Errors { + // we remove all http request rules because of permanent backend still in parsers. + _, existingHTTPRequestRules, _ := configuration.GetHTTPRequestRules("backend", backendName, c.activeTransaction) + for range existingHTTPRequestRules { + _ = configuration.DeleteHTTPRequestRule(0, "backend", backendName, c.activeTransaction, 0) + } + var errs utils.Errors + // we (re)create all http request rules + for _, httpRequestRule := range httpRequestsRules { + errs.Add(configuration.CreateHTTPRequestRule("backend", backendName, httpRequestRule, c.activeTransaction, 0)) + } + return errs +} + +func (c *clientNative) PushPreviousBackends() error { + logger.Debug("Pushing backends as previous successfully applied backends") + jsonBackends, err := json.Marshal(c.backends) //nolint:musttag + if err != nil { + return err + } + c.previousBackends = jsonBackends + return nil +} + +func (c *clientNative) PopPreviousBackends() error { + logger.Debug("Popping backends from previous successfully applied backends") + if c.previousBackends == nil { + clear(c.backends) + return nil + } + backends := map[string]Backend{} + err := json.Unmarshal(c.previousBackends, &backends) //nolint:musttag + if err != nil { + return err + } + c.backends = backends + return nil +} diff --git a/pkg/haproxy/api/backend.go b/pkg/haproxy/api/backend.go index b6d26003..774a393d 100644 --- a/pkg/haproxy/api/backend.go +++ b/pkg/haproxy/api/backend.go @@ -2,137 +2,112 @@ package api import ( "errors" + "fmt" + "slices" + "strings" - "github.com/haproxytech/client-native/v5/config-parser/types" "github.com/haproxytech/client-native/v5/models" "github.com/haproxytech/kubernetes-ingress/pkg/utils" ) -func (c *clientNative) BackendsGet() (models.Backends, error) { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return nil, err +var ErrNotFound = errors.New("not found") + +func (c *clientNative) BackendsGet() models.Backends { + backends := models.Backends(make([]*models.Backend, len(c.backends))) + i := 0 + for _, backend := range c.backends { + backends[i] = &backend.BackendBase + i++ } - _, backends, err := configuration.GetBackends(c.activeTransaction) - return backends, err + return backends } func (c *clientNative) BackendGet(backendName string) (*models.Backend, error) { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return nil, err + oldBackend, ok := c.backends[backendName] + if ok { + return &oldBackend.BackendBase, nil } - _, backend, err := configuration.GetBackend(backendName, c.activeTransaction) - if err != nil { - return nil, err - } - c.activeBackends[backend.Name] = struct{}{} - return backend, nil + return nil, fmt.Errorf("backend %s not found", backendName) } -func (c *clientNative) BackendCreate(backend models.Backend) error { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return err - } - c.activeTransactionHasChanges = true - err = configuration.CreateBackend(&backend, c.activeTransaction, 0) - if err != nil { - return err - } - c.activeBackends[backend.Name] = struct{}{} - return nil +func (c *clientNative) BackendCreatePermanently(backend models.Backend) { + c.BackendCreateOrUpdate(backend) + newBackend := c.backends[backend.Name] + newBackend.Permanent = true + c.backends[backend.Name] = newBackend } -func (c *clientNative) BackendCreatePermanently(backend models.Backend) error { - err := c.BackendCreate(backend) - if err != nil { - return err +func (c *clientNative) BackendCreateIfNotExist(backend models.Backend) { + existingBackend := c.backends[backend.Name] + existingBackend.Used = true + c.backends[backend.Name] = existingBackend + if c.BackendExists(backend.Name) { + return } - c.permanentBackends[backend.Name] = struct{}{} - return nil + c.BackendCreateOrUpdate(backend) } -func (c *clientNative) BackendCreateIfNotExist(backend models.Backend) (err error) { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return - } - c.activeTransactionHasChanges = true - defer func() { - if err == nil { - c.activeBackends[backend.Name] = struct{}{} +func (c *clientNative) BackendCreateOrUpdate(backend models.Backend) (diff map[string][]interface{}, created bool) { + oldBackend, ok := c.backends[backend.Name] + if !ok { + c.backends[backend.Name] = Backend{ + BackendBase: backend, + Used: true, } - }() - - _, _, err = configuration.GetBackend(backend.Name, c.activeTransaction) - if err == nil { - return + return nil, true } - return configuration.CreateBackend(&backend, c.activeTransaction, 0) -} + diff = oldBackend.BackendBase.Diff(backend) -func (c *clientNative) BackendEdit(backend models.Backend) error { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return err - } - c.activeTransactionHasChanges = true - return configuration.EditBackend(backend.Name, &backend, c.activeTransaction, 0) + c.activeTransactionHasChanges = len(diff) > 0 + + oldBackend.BackendBase = backend + oldBackend.Used = true + c.backends[backend.Name] = oldBackend + return diff, false } -func (c *clientNative) BackendDelete(backendName string) error { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return err +func (c *clientNative) BackendDelete(backendName string) { + backend, exists := c.backends[backendName] + if !exists { + return } - c.activeTransactionHasChanges = true - return configuration.DeleteBackend(backendName, c.activeTransaction, 0) + backend.Used = false + backend.Permanent = false + c.backends[backendName] = backend } func (c *clientNative) BackendCfgSnippetSet(backendName string, value []string) error { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return err - } - config, err := configuration.GetParser(c.activeTransaction) - if err != nil { - return err - } - if len(value) == 0 { - err = config.Set("backend", backendName, "config-snippet", nil) - } else { - err = config.Set("backend", backendName, "config-snippet", types.StringSliceC{Value: value}) + backend, exists := c.backends[backendName] + if !exists { + return fmt.Errorf("backend %s : %w", backendName, ErrNotFound) } - if err != nil { - c.activeTransactionHasChanges = true - } - return err + + c.activeTransactionHasChanges = slices.Compare(backend.ConfigSnippets, value) != 0 + backend.ConfigSnippets = value + c.backends[backendName] = backend + return nil } -func (c *clientNative) BackendHTTPRequestRuleCreate(backend string, rule models.HTTPRequestRule) error { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return err +// To remove ? +func (c *clientNative) BackendHTTPRequestRuleCreate(backendName string, rule models.HTTPRequestRule) error { + backend, exists := c.backends[backendName] + if !exists { + return fmt.Errorf("can't add http request rule for unexisting backend %s, %w", backendName, ErrNotFound) } - c.activeTransactionHasChanges = true - return configuration.CreateHTTPRequestRule("backend", backend, &rule, c.activeTransaction, 0) + backend.HTTPRequestsRules = append(backend.HTTPRequestsRules, &rule) + c.backends[backendName] = backend + return nil } -func (c *clientNative) BackendServerDeleteAll(backendName string) bool { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - logger := utils.GetLogger() - logger.Error(err) - return false - } - _, servers, _ := configuration.GetServers("backend", backendName, c.activeTransaction) - for _, srv := range servers { - c.activeTransactionHasChanges = true - _ = c.BackendServerDelete(backendName, srv.Name) +func (c *clientNative) BackendServerDeleteAll(backendName string) error { + backend, exists := c.backends[backendName] + if !exists { + return fmt.Errorf("can't delete servers from unexisting backend %s", backendName) } - return c.activeTransactionHasChanges + backend.Servers = nil + c.backends[backendName] = backend + return nil } func (c *clientNative) BackendRuleDeleteAll(backend string) { @@ -151,84 +126,143 @@ func (c *clientNative) BackendRuleDeleteAll(backend string) { } } +func (c *clientNative) BackendServerCreateOrUpdate(backendName string, data models.Server) error { + backend, exists := c.backends[backendName] + if !exists { + return fmt.Errorf("can't create server for unexisting backend %s", backendName) + } + if data.Name == "" { + return fmt.Errorf("can't create unnamed server in backend %s", backendName) + } + + if backend.Servers == nil { + backend.Servers = map[string]models.Server{} + } + backend.Servers[data.Name] = data + c.backends[backendName] = backend + return nil +} + func (c *clientNative) BackendServerCreate(backendName string, data models.Server) error { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return err + backend, exists := c.backends[backendName] + if !exists { + return fmt.Errorf("can't create server for unexisting backend %s", backendName) } - c.activeTransactionHasChanges = true - return configuration.CreateServer("backend", backendName, &data, c.activeTransaction, 0) + if data.Name == "" { + return fmt.Errorf("can't create unnamed server in backend %s", backendName) + } + + _, exists = backend.Servers[data.Name] + if exists { + return fmt.Errorf("can't create already existing server %s in backend %s", data.Name, backendName) + } + if backend.Servers == nil { + backend.Servers = map[string]models.Server{} + } + backend.Servers[data.Name] = data + c.backends[backendName] = backend + return nil } func (c *clientNative) BackendServerEdit(backendName string, data models.Server) error { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return err + backend, exists := c.backends[backendName] + if !exists { + return fmt.Errorf("can't edit server for unexisting backend %s, %w", backendName, ErrNotFound) + } + if data.Name == "" { + return fmt.Errorf("can't edit unnamed server in backend %s", backendName) } - c.activeTransactionHasChanges = true - return configuration.EditServer(data.Name, "backend", backendName, &data, c.activeTransaction, 0) -} -func (c *clientNative) BackendServerCreateOrEdit(backendName string, data models.Server) error { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return err + if backend.Servers == nil { + return fmt.Errorf("can't edit unexisting server %s in backend %s, %w", data.Name, backendName, ErrNotFound) } - c.activeTransactionHasChanges = true - return configuration.CreateOrEditServer("backend", backendName, &data, c.activeTransaction, 0) + _, exists = backend.Servers[data.Name] + if !exists { + return fmt.Errorf("can't edit unexisting server %s in backend %s, %w", data.Name, backendName, ErrNotFound) + } + backend.Servers[data.Name] = data + c.backends[backendName] = backend + return nil } func (c *clientNative) BackendServerDelete(backendName string, serverName string) error { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return err + backend, exists := c.backends[backendName] + if !exists { + return fmt.Errorf("can't edit server for unexisting backend %s", backendName) } - c.activeTransactionHasChanges = true - return configuration.DeleteServer(serverName, "backend", backendName, c.activeTransaction, 0) + if serverName == "" { + return fmt.Errorf("can't edit unnamed server in backend %s", backendName) + } + + _, exists = backend.Servers[serverName] + if !exists { + return fmt.Errorf("can't delete unexisting server %s in backend %s", serverName, backendName) + } + delete(backend.Servers, serverName) + c.backends[backendName] = backend + return nil } -func (c *clientNative) ServerGet(serverName, backendName string) (models.Server, error) { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return models.Server{}, err +func (c *clientNative) BackendServerGet(serverName, backendName string) (*models.Server, error) { + backend, exists := c.backends[backendName] + if !exists { + return nil, fmt.Errorf("can't get server %s for unexisting backend %s", serverName, backendName) } - _, server, err := configuration.GetServer(serverName, "backend", backendName, c.activeTransaction) - if err != nil { - return models.Server{}, err + if serverName == "" { + return nil, fmt.Errorf("can't get unnamed server in backend %s", backendName) + } + + server, exists := backend.Servers[serverName] + if !exists { + return nil, nil //nolint:golint,nilnil } - return *server, nil + return &server, nil } func (c *clientNative) BackendServersGet(backendName string) (models.Servers, error) { - configuration, err := c.nativeAPI.Configuration() - if err != nil { - return nil, err + backend, exists := c.backends[backendName] + if !exists { + return nil, fmt.Errorf("can't get server for unexisting backend %s", backendName) } - _, servers, err := configuration.GetServers("backend", backendName, c.activeTransaction) - if err != nil { - return nil, err + servers := models.Servers(make([]*models.Server, len(backend.Servers))) + i := 0 + for _, server := range backend.Servers { + servers[i] = &server + i++ } + slices.SortFunc(servers, func(a, b *models.Server) int { + lenDiff := len(a.Name) - len(b.Name) + if lenDiff != 0 { + return lenDiff + } + return strings.Compare(a.Name, b.Name) + }) return servers, nil } -func (c *clientNative) RefreshBackends() (deleted []string, err error) { - backends, errAPI := c.BackendsGet() - if errAPI != nil { - err = errors.New("unable to get configured backends") - return +func (c *clientNative) BackendExists(backendName string) (exists bool) { + _, exists = c.backends[backendName] + return +} + +func (c *clientNative) BackendDeleteAllUnnecessary() ([]string, error) { + configuration, err := c.nativeAPI.Configuration() + if err != nil { + return nil, err } - for _, backend := range backends { - if _, ok := c.permanentBackends[backend.Name]; ok { + + c.activeTransactionHasChanges = true + var errs utils.Errors + var backendDeleted []string //nolint:prealloc + for _, backend := range c.backends { + // if a backend is not permanent and has not been "viewed" in the transacton then remove it. + if backend.Used || backend.Permanent { continue } - if _, ok := c.activeBackends[backend.Name]; !ok { - if err = c.BackendDelete(backend.Name); err != nil { - return - } - utils.GetLogger().Debugf("backend '%s' deleted", backend.Name) - deleted = append(deleted, backend.Name) - } + backendName := backend.BackendBase.Name + delete(c.backends, backendName) + _ = configuration.DeleteBackend(backendName, c.activeTransaction, 0) + backendDeleted = append(backendDeleted, backendName) } - c.activeBackends = map[string]struct{}{} - return + return backendDeleted, errs.Result() } diff --git a/pkg/haproxy/api/httprequest.go b/pkg/haproxy/api/httprequest.go index d7c5e8a1..a3eec8ad 100644 --- a/pkg/haproxy/api/httprequest.go +++ b/pkg/haproxy/api/httprequest.go @@ -1,12 +1,23 @@ package api -import "github.com/haproxytech/client-native/v5/models" +import ( + "fmt" + + "github.com/haproxytech/client-native/v5/models" +) func (c *clientNative) HTTPRequestRulesGet(parentType, parentName string) (models.HTTPRequestRules, error) { configuration, err := c.nativeAPI.Configuration() if err != nil { return nil, err } + if parentType == "backend" { + backend, exists := c.backends[parentName] + if !exists { + return nil, fmt.Errorf("can't get http requests rules for unexisting backend %s : %w", parentName, ErrNotFound) + } + return backend.HTTPRequestsRules, nil + } _, httpRequests, err := configuration.GetHTTPRequestRules(parentType, parentName, c.activeTransaction) if err != nil { return nil, err @@ -39,6 +50,15 @@ func (c *clientNative) HTTPRequestRuleDeleteAll(parentType string, parentName st if err != nil { return err } + if parentType == "backend" { + backend, exists := c.backends[parentName] + if !exists { + return fmt.Errorf("can't delete http requests rules for unexisting backend %s : %w", parentName, ErrNotFound) + } + backend.HTTPRequestsRules = nil + c.backends[parentName] = backend + return nil + } _, httpRequests, errGet := configuration.GetHTTPRequestRules(parentType, parentName, c.activeTransaction) if errGet != nil { return errGet @@ -58,6 +78,15 @@ func (c *clientNative) HTTPRequestRuleCreate(parentType string, parentName strin if err != nil { return err } + if parentType == "backend" { + backend, exists := c.backends[parentName] + if !exists { + return fmt.Errorf("can't create http request rule for unexisting backend %s : %w", parentName, ErrNotFound) + } + backend.HTTPRequestsRules = append(backend.HTTPRequestsRules, data) + c.backends[parentName] = backend + return nil + } return configuration.CreateHTTPRequestRule(parentType, parentName, data, c.activeTransaction, 0) } diff --git a/pkg/haproxy/process/direct-control.go b/pkg/haproxy/process/direct-control.go index e4072252..22d7fad9 100644 --- a/pkg/haproxy/process/direct-control.go +++ b/pkg/haproxy/process/direct-control.go @@ -37,9 +37,9 @@ func (d *directControl) Service(action string) (err error) { logger.Error("haproxy is already running") return nil } - cmd = exec.Command(d.Env.Binary, "-S", masterSocketArg, "-f", d.Env.MainCFGFile) + cmd = exec.Command(d.Env.Binary, "-W", "-S", masterSocketArg, "-f", d.Env.MainCFGFile) if d.useAuxFile { - cmd = exec.Command(d.Env.Binary, "-S", masterSocketArg, "-f", d.Env.MainCFGFile, "-f", d.Env.AuxCFGFile) + cmd = exec.Command(d.Env.Binary, "-W", "-S", masterSocketArg, "-f", d.Env.MainCFGFile, "-f", d.Env.AuxCFGFile) } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/pkg/haproxy/rules/reqTrack.go b/pkg/haproxy/rules/reqTrack.go index eb841830..173bf35b 100644 --- a/pkg/haproxy/rules/reqTrack.go +++ b/pkg/haproxy/rules/reqTrack.go @@ -27,20 +27,15 @@ func (r ReqTrack) Create(client api.HAProxyClient, frontend *models.Frontend, in } // Create tracking table. - if _, err := client.BackendGet(r.TableName); err != nil { - err = client.BackendCreate(models.Backend{ - Name: r.TableName, - StickTable: &models.ConfigStickTable{ - Peers: "localinstance", - Type: "ip", - Size: r.TableSize, - Store: fmt.Sprintf("http_req_rate(%d)", *r.TablePeriod), - }, - }) - if err != nil { - return err - } - } + client.BackendCreateOrUpdate(models.Backend{ + Name: r.TableName, + StickTable: &models.ConfigStickTable{ + Peers: "localinstance", + Type: "ip", + Size: r.TableSize, + Store: fmt.Sprintf("http_req_rate(%d)", *r.TablePeriod), + }, + }) // Create rule httpRule := models.HTTPRequestRule{ Index: utils.PtrInt64(0), diff --git a/pkg/service/endpoints.go b/pkg/service/endpoints.go index 275df12c..4fb98f46 100644 --- a/pkg/service/endpoints.go +++ b/pkg/service/endpoints.go @@ -36,7 +36,7 @@ func (s *Service) HandleHAProxySrvs(k8s store.K8s, client api.HAProxyClient) { } logger.Warningf("Ingress '%s/%s': %s", s.resource.Namespace, s.resource.Name, err) if servers, _ := client.BackendServersGet(s.backend.Name); servers != nil { - client.BackendServerDeleteAll(s.backend.Name) + _ = client.BackendServerDeleteAll(s.backend.Name) } return } @@ -74,7 +74,7 @@ func (s *Service) updateHAProxySrv(client api.HAProxyClient, srvSlot store.HAPro } logger.Tracef("[CONFIG] [BACKEND] [SERVER] backend %s: about to update server in configuration file : models.Server { Name: %s, Port: %d, Address: %s, Maintenance: %s }", s.backend.Name, srv.Name, *srv.Port, srv.Address, srv.Maintenance) - errAPI := client.BackendServerCreateOrEdit(s.backend.Name, srv) + errAPI := client.BackendServerCreateOrUpdate(s.backend.Name, srv) if errAPI == nil { logger.Tracef("[CONFIG] [BACKEND] [SERVER] Creating/Updating server '%s/%s'", s.backend.Name, srv.Name) } diff --git a/pkg/service/service.go b/pkg/service/service.go index 3e0e170a..c5add8e7 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -141,7 +141,6 @@ func (s *Service) GetBackendName() (name string, err error) { // HandleBackend processes a Service and creates/updates corresponding backend configuration in HAProxy func (s *Service) HandleBackend(storeK8s store.K8s, client api.HAProxyClient, a annotations.Annotations) (err error) { - var backend *models.Backend var newBackend *v1.BackendSpec newBackend, err = s.getBackendModel(storeK8s, a) if err != nil { @@ -149,27 +148,14 @@ func (s *Service) HandleBackend(storeK8s store.K8s, client api.HAProxyClient, a return } s.backend = newBackend.Config + backend, _ := client.BackendGet(newBackend.Config.Name) // Get/Create Backend - backend, err = client.BackendGet(newBackend.Config.Name) - if err == nil { - // Update Backend - diff := newBackend.Config.Diff(*backend) - if len(diff) != 0 { - // Detect if we have a diff on the server line - if isServersToEdit(newBackend.Config, backend) { - s.serversToEdit = true - } - if err = client.BackendEdit(*newBackend.Config); err != nil { - return - } - instance.Reload("Service '%s/%s': backend '%s' updated: %v", s.resource.Namespace, s.resource.Name, newBackend.Config.Name, diff) - } - } else { - if err = client.BackendCreate(*newBackend.Config); err != nil { - return - } - s.newBackend = true - instance.Reload(fmt.Sprintf("Service '%s/%s': new backend '%s'", s.resource.Namespace, s.resource.Name, newBackend.Config.Name)) + diff, created := client.BackendCreateOrUpdate(*newBackend.Config) + instance.ReloadIf(len(diff) > 0 || created, "Service '%s/%s': backend '%s' upserted: %v", s.resource.Namespace, s.resource.Name, newBackend.Config.Name, diff) + s.newBackend = created + // if updated but not created + if len(diff) > 0 && !created { + s.serversToEdit = isServersToEdit(newBackend.Config, backend) } // acls acls.PopulateBackend(client, newBackend.Config.Name, newBackend.Acls) diff --git a/pkg/utils/errors.go b/pkg/utils/errors.go index f6ddd0c7..ff5053f8 100644 --- a/pkg/utils/errors.go +++ b/pkg/utils/errors.go @@ -24,3 +24,9 @@ func (e *Errors) Result() error { } return errors.New(result) } + +func (e *Errors) AddErrors(errors Errors) { + for _, err := range errors { + e.Add((err)) + } +}