HAProxy Data Plane API
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

534 lines
12 KiB

// Copyright 2019 HAProxy Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package haproxy
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/google/renameio"
client_native "github.com/haproxytech/client-native/v6"
"github.com/haproxytech/client-native/v6/misc"
"github.com/haproxytech/client-native/v6/models"
"github.com/haproxytech/client-native/v6/runtime"
"github.com/haproxytech/dataplaneapi/log"
)
const (
logFieldReloadID = "reload_id"
)
type IReloadAgent interface {
Reload() string
ReloadWithCallback(func()) string
Restart() error
ForceReload() error
ForceReloadWithCallback(func()) error
Status() (bool, error)
GetReloads() models.Reloads
GetReload(id string) *models.Reload
}
type reloadCache struct {
failedReloads map[string]*models.Reload
lastSuccess *models.Reload
callbacks map[string]func()
next string
current string
index int64
retention int
mu sync.RWMutex
}
type ReloadAgentParams struct {
Client client_native.HAProxyClient
Ctx context.Context
ReloadCmd string
RestartCmd string
StatusCmd string
ConfigFile string
BackupDir string
Delay int
Retention int
UseMasterSocket bool
}
// ReloadAgent handles all reloads, scheduled or forced
type ReloadAgent struct {
runtime runtime.Runtime
done <-chan struct{}
reloadCmd string
restartCmd string
statusCmd string
configFile string
lkgConfigFile string
cache reloadCache
delay int
useMasterSocket bool
}
func NewReloadAgent(params ReloadAgentParams) (*ReloadAgent, error) {
ra := &ReloadAgent{}
ra.reloadCmd = params.ReloadCmd
ra.useMasterSocket = params.UseMasterSocket
ra.restartCmd = params.RestartCmd
ra.statusCmd = params.StatusCmd
ra.configFile = params.ConfigFile
if params.Ctx == nil {
params.Ctx = context.Background()
}
ra.done = params.Ctx.Done()
if ra.useMasterSocket {
rt, err := params.Client.Runtime()
if err != nil {
return nil, err
}
ra.runtime = rt
}
params.Delay *= 1000 // delay is defined in seconds - internally in miliseconds
d := os.Getenv("CI_DATAPLANE_RELOAD_DELAY_OVERRIDE")
if d != "" {
params.Delay, _ = strconv.Atoi(d) // in case of err in conversion 0 is returned
}
if params.Delay == 0 {
params.Delay = 5000
}
ra.delay = params.Delay
if err := ra.setLkgPath(params.ConfigFile, params.BackupDir); err != nil {
return nil, err
}
// create last known good file, assume it is valid when starting
if err := copyFile(ra.configFile, ra.lkgConfigFile); err != nil {
return nil, err
}
ra.cache.Init(params.Retention)
go ra.handleReloads()
return ra, nil
}
func (ra *ReloadAgent) setLkgPath(configFile, path string) error {
if path != "" {
var err error
path, err = misc.CheckOrCreateWritableDirectory(path)
if err != nil {
return err
}
ra.lkgConfigFile = fmt.Sprintf("%s/%s.lkg", path, filepath.Base(configFile))
return nil
}
ra.lkgConfigFile = configFile + ".lkg"
return nil
}
func (ra *ReloadAgent) handleReload(id string) (string, error) {
logFields := map[string]interface{}{logFieldReloadID: id}
ra.cache.mu.Lock()
ra.cache.current = id
defer func() {
ra.cache.next = ""
ra.cache.mu.Unlock()
}()
response, err := ra.reloadHAProxy(id)
if err != nil {
ra.cache.failReload(response)
log.WithFieldsf(logFields, log.WarnLevel, "Reload failed: %s", err)
} else {
ra.cache.succeedReload(response)
callback, ok := ra.cache.callbacks[id]
if ok {
callback()
}
log.WithFields(logFields, log.DebugLevel, "Handling reload completed, waiting for new requests")
}
delete(ra.cache.callbacks, id)
return response, err
}
func (ra *ReloadAgent) handleReloads() {
ticker := time.NewTicker(time.Duration(ra.delay) * time.Millisecond)
for {
select {
case <-ticker.C:
if next := ra.cache.getNext(); next != "" {
ra.handleReload(next) //nolint:errcheck
}
case <-ra.done:
ticker.Stop()
return
}
}
}
func (ra *ReloadAgent) reloadHAProxy(id string) (string, error) {
logFields := map[string]interface{}{logFieldReloadID: id}
// try the reload
log.WithFields(logFields, log.DebugLevel, "Reload started")
var output string
var err error
t := time.Now()
if ra.useMasterSocket {
output, err = ra.runtime.Reload()
} else {
output, err = execCmd(ra.reloadCmd)
}
if err != nil {
reloadFailedError := err
// If failed, return to last known good file.
log.WithFields(logFields, log.InfoLevel, "Reload failed, reverting the last working config...")
if err := copyFile(ra.configFile, ra.configFile+".bck"); err != nil {
return fmt.Sprintf("Reload failed: %s. Failed to backup the current config file.", output), err
}
defer func() {
os.Remove(ra.configFile + ".bck")
}()
if err := copyFile(ra.lkgConfigFile, ra.configFile); err != nil {
return fmt.Sprintf("Reload failed: %s. Failed to revert to the last working config file.", output), err
}
return output, reloadFailedError
}
log.WithFieldsf(logFields, log.DebugLevel, "Reload finished in %s", time.Since(t))
log.WithFields(logFields, log.DebugLevel, "Reload successful")
// if success, replace last known good file
copyFile(ra.configFile, ra.lkgConfigFile) //nolint:errcheck
return output, nil
}
func (ra *ReloadAgent) restartHAProxy() error {
_, err := execCmd(ra.restartCmd)
return err
}
func execCmd(cmd string) (string, error) {
strArr := strings.Split(cmd, " ")
var c *exec.Cmd
if len(strArr) == 1 {
//nolint:gosec
c = exec.Command(strArr[0])
} else {
//nolint:gosec
c = exec.Command(strArr[0], strArr[1:]...)
}
var stdout, stderr bytes.Buffer
c.Stdout = &stdout
c.Stderr = &stderr
err := c.Run()
if err != nil {
return stderr.String(), fmt.Errorf("executing %s failed: %s", cmd, err)
}
return stdout.String(), nil
}
// Reload schedules a reload
func (ra *ReloadAgent) Reload() string {
next := ra.cache.getNext()
if next == "" {
next = ra.cache.newReload()
log.WithFields(map[string]interface{}{logFieldReloadID: next}, log.DebugLevel, "Scheduling a new reload...")
}
return next
}
// ForceReload calls reload directly
func (ra *ReloadAgent) ForceReload() error {
next := ra.cache.getNext()
if next != "" {
r, err := ra.handleReload(next)
if err != nil {
return NewReloadError(fmt.Sprintf("Reload failed: %v, %v", err, r))
}
return nil
}
r, err := ra.reloadHAProxy("force")
if err != nil {
return NewReloadError(fmt.Sprintf("Reload failed: %v, %v", err, r))
}
return nil
}
// Reload schedules a reload, callback is called only if reload is successful
func (ra *ReloadAgent) ReloadWithCallback(callback func()) string {
next := ra.cache.getNext()
if next == "" {
next = ra.cache.newReloadWithCallback(callback)
log.WithFields(map[string]interface{}{logFieldReloadID: next}, log.DebugLevel, "Scheduling a new reload...")
}
ra.cache.mu.Lock()
ra.cache.callbacks[next] = callback
ra.cache.mu.Unlock()
return next
}
// ForceReload calls reload directly, callback is called only if reload is successful
func (ra *ReloadAgent) ForceReloadWithCallback(callback func()) error {
next := ra.cache.getNext()
if next != "" {
r, err := ra.handleReload(next)
if err != nil {
return NewReloadError(fmt.Sprintf("Reload failed: %v, %v", err, r))
}
callback()
return nil
}
r, err := ra.reloadHAProxy("force")
if err != nil {
return NewReloadError(fmt.Sprintf("Reload failed: %v, %v", err, r))
}
callback()
return nil
}
func (rc *reloadCache) Init(retention int) {
rc.mu.Lock()
defer rc.mu.Unlock()
rc.failedReloads = make(map[string]*models.Reload)
rc.current = ""
rc.next = ""
rc.lastSuccess = nil
rc.index = 0
rc.retention = retention
rc.callbacks = make(map[string]func())
}
func (rc *reloadCache) newReload() string {
rc.mu.Lock()
defer rc.mu.Unlock()
id := fmt.Sprintf("%s-%v", time.Now().Format("2006-01-02"), rc.index)
rc.index++
rc.next = id
return rc.next
}
func (rc *reloadCache) newReloadWithCallback(callback func()) string {
next := rc.newReload()
rc.mu.Lock()
rc.callbacks[next] = callback
rc.mu.Unlock()
return next
}
func (rc *reloadCache) getNext() string {
rc.mu.RLock()
defer rc.mu.RUnlock()
return rc.next
}
func (rc *reloadCache) failReload(response string) {
r := &models.Reload{
ID: rc.current,
Status: models.ReloadStatusFailed,
Response: response,
ReloadTimestamp: time.Now().Unix(),
}
rc.failedReloads[rc.current] = r
rc.current = ""
rc.clearReloads()
}
func (rc *reloadCache) succeedReload(response string) {
r := &models.Reload{
ID: rc.current,
Status: models.ReloadStatusSucceeded,
Response: response,
ReloadTimestamp: time.Now().Unix(),
}
rc.lastSuccess = r
rc.current = ""
}
func (rc *reloadCache) clearReloads() {
now := time.Now().Unix()
for k, v := range rc.failedReloads {
if (now - v.ReloadTimestamp) > int64((rc.retention * 86400)) {
delete(rc.failedReloads, k)
}
}
}
func (ra *ReloadAgent) GetReloads() models.Reloads {
ra.cache.mu.RLock()
defer ra.cache.mu.RUnlock()
v := make([]*models.Reload, 0, len(ra.cache.failedReloads))
for _, value := range ra.cache.failedReloads {
v = append(v, value)
}
if ra.cache.lastSuccess != nil {
v = append(v, ra.cache.lastSuccess)
}
if ra.cache.current != "" {
r := &models.Reload{
ID: ra.cache.current,
Status: models.ReloadStatusInProgress,
}
v = append(v, r)
}
if ra.cache.next != "" {
r := &models.Reload{
ID: ra.cache.next,
Status: models.ReloadStatusInProgress,
}
v = append(v, r)
}
return v
}
func (ra *ReloadAgent) GetReload(id string) *models.Reload {
ra.cache.mu.RLock()
defer ra.cache.mu.RUnlock()
if ra.cache.current == id {
return &models.Reload{
ID: ra.cache.current,
Status: models.ReloadStatusInProgress,
}
}
if ra.cache.next == id {
return &models.Reload{
ID: ra.cache.current,
Status: models.ReloadStatusInProgress,
}
}
v, ok := ra.cache.failedReloads[id]
if ok {
return v
}
if ra.cache.lastSuccess != nil {
if ra.cache.lastSuccess.ID == id {
return ra.cache.lastSuccess
}
// if it is older than last success return success
sDate, sIndex, err := getTimeIndexFromID(ra.cache.lastSuccess.ID)
if err != nil {
return nil
}
gDate, gIndex, err := getTimeIndexFromID(id)
if err != nil {
return nil
}
if gDate.Before(sDate) {
return &models.Reload{
ID: id,
Status: models.ReloadStatusSucceeded,
}
}
if sIndex > gIndex {
return &models.Reload{
ID: id,
Status: models.ReloadStatusSucceeded,
}
}
}
return nil
}
func (ra *ReloadAgent) Restart() error {
return ra.restartHAProxy()
}
func (ra *ReloadAgent) Status() (bool, error) {
return ra.status()
}
func (ra *ReloadAgent) status() (bool, error) {
if ra.statusCmd == "" {
return false, fmt.Errorf("status command not configured")
}
resp, err := execCmd(ra.statusCmd)
if err != nil {
log.Debugf("haproxy status check failed: %s", resp)
return false, nil //nolint:nilerr
}
log.Debugf("haproxy status check successful: %s", resp)
return true, nil
}
func getTimeIndexFromID(id string) (time.Time, int64, error) {
data := strings.Split(id, "-")
index, err := strconv.ParseInt(data[len(data)-1], 10, 64)
if err != nil {
return time.Now(), 0, err
}
date, err := time.Parse("2006-01-02", strings.Join(data[:len(data)-1], "-"))
if err != nil {
return date, 0, err
}
return date, index, nil
}
// ReloadError general configuration client error
type ReloadError struct {
msg string
}
// Error implementation for ConfError
func (e *ReloadError) Error() string {
return fmt.Sprintf(e.msg)
}
// NewReloadError constructor for ReloadError
func NewReloadError(msg string) *ReloadError {
return &ReloadError{msg: msg}
}
func copyFile(src, dest string) error {
srcContent, err := os.Open(src)
if err != nil {
return err
}
defer srcContent.Close()
data, err := io.ReadAll(srcContent)
if err != nil {
return err
}
return renameio.WriteFile(dest, data, 0o644)
}