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.
330 lines
11 KiB
330 lines
11 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 handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/go-openapi/runtime/middleware"
|
|
client_native "github.com/haproxytech/client-native/v6"
|
|
"github.com/haproxytech/client-native/v6/models"
|
|
|
|
"github.com/haproxytech/dataplaneapi/configuration"
|
|
"github.com/haproxytech/dataplaneapi/haproxy"
|
|
"github.com/haproxytech/dataplaneapi/operations/cluster"
|
|
)
|
|
|
|
// CreateClusterHandlerImpl implementation of the CreateClusterHandler interface
|
|
type CreateClusterHandlerImpl struct {
|
|
Client client_native.HAProxyClient
|
|
Config *configuration.Configuration
|
|
ReloadAgent haproxy.IReloadAgent
|
|
}
|
|
|
|
// GetClusterHandlerImpl implementation of the GetClusterHandler interface
|
|
type GetClusterHandlerImpl struct {
|
|
Config *configuration.Configuration
|
|
}
|
|
|
|
// ClusterInitiateCertificateRefreshHandlerImpl implementation of the ClusterInitiateCertificateRefreshHandler interface
|
|
type ClusterInitiateCertificateRefreshHandlerImpl struct {
|
|
Config *configuration.Configuration
|
|
}
|
|
|
|
type DeleteClusterHandlerImpl struct {
|
|
Client client_native.HAProxyClient
|
|
Config *configuration.Configuration
|
|
Users *configuration.Users
|
|
ReloadAgent haproxy.IReloadAgent
|
|
}
|
|
|
|
type EditClusterHandlerImpl struct {
|
|
Config *configuration.Configuration
|
|
}
|
|
|
|
// Handle executing the request and returning a response
|
|
func (h *ClusterInitiateCertificateRefreshHandlerImpl) Handle(params cluster.InitiateCertificateRefreshParams, principal interface{}) middleware.Responder {
|
|
if h.Config.Mode.Load() != configuration.ModeCluster {
|
|
return cluster.NewInitiateCertificateRefreshForbidden()
|
|
}
|
|
h.Config.Notify.CertificateRefresh.Notify()
|
|
return cluster.NewInitiateCertificateRefreshOK()
|
|
}
|
|
|
|
func (h *CreateClusterHandlerImpl) err500(err error, transaction *models.Transaction) middleware.Responder {
|
|
if transaction != nil {
|
|
configuration, confErr := h.Client.Configuration()
|
|
if confErr == nil {
|
|
_ = configuration.DeleteTransaction(transaction.ID)
|
|
}
|
|
}
|
|
msg := err.Error()
|
|
code := int64(500)
|
|
return cluster.NewPostClusterDefault(500).WithPayload(&models.Error{
|
|
Code: &code,
|
|
Message: &msg,
|
|
})
|
|
}
|
|
|
|
func (h *CreateClusterHandlerImpl) err406(err error, transaction *models.Transaction) middleware.Responder {
|
|
// 406 Not Acceptable
|
|
if transaction != nil {
|
|
configuration, confErr := h.Client.Configuration()
|
|
if confErr == nil {
|
|
_ = configuration.DeleteTransaction(transaction.ID)
|
|
}
|
|
}
|
|
msg := err.Error()
|
|
code := int64(406)
|
|
return cluster.NewPostClusterDefault(406).WithPayload(&models.Error{
|
|
Code: &code,
|
|
Message: &msg,
|
|
})
|
|
}
|
|
|
|
func (h *CreateClusterHandlerImpl) err409(err error, transaction *models.Transaction) middleware.Responder {
|
|
// 409 Conflict
|
|
if transaction != nil {
|
|
configuration, confErr := h.Client.Configuration()
|
|
if confErr == nil {
|
|
_ = configuration.DeleteTransaction(transaction.ID)
|
|
}
|
|
}
|
|
msg := err.Error()
|
|
code := int64(409)
|
|
return cluster.NewPostClusterDefault(409).WithPayload(&models.Error{
|
|
Code: &code,
|
|
Message: &msg,
|
|
})
|
|
}
|
|
|
|
func (h *CreateClusterHandlerImpl) Handle(params cluster.PostClusterParams, principal interface{}) middleware.Responder {
|
|
key := h.Config.Cluster.BootstrapKey.Load()
|
|
if params.Data.BootstrapKey != "" && key != params.Data.BootstrapKey {
|
|
// before we switch to cluster mode, check if folder for storage is compatible with dataplane
|
|
key, err := configuration.DecodeBootstrapKey(params.Data.BootstrapKey)
|
|
if err != nil {
|
|
return h.err406(err, nil)
|
|
}
|
|
log.Warningf("received instructions from %s to join cluster %s at %s", params.HTTPRequest.RemoteAddr, key["name"], key["address"])
|
|
errStorageDir := configuration.CheckIfStorageDirIsOK(key["storage-dir"], h.Config)
|
|
if errStorageDir != nil {
|
|
log.Warningf("configured storage dir incompatible with cluster configuration: %s", errStorageDir)
|
|
return h.err409(errStorageDir, nil)
|
|
}
|
|
// Init NOTICE file to inform user that the cluster storage folder is programmatically managed by Fusion API
|
|
if errStorageInit := configuration.InitStorageNoticeFile(key["storage-dir"]); errStorageInit != nil {
|
|
log.Warningf("unable to create notice file, %s: skipping it", errStorageInit.Error())
|
|
}
|
|
// enforcing API advertising options
|
|
if a := params.AdvertisedAddress; a != nil {
|
|
h.Config.APIOptions.APIAddress = *a
|
|
}
|
|
if p := params.AdvertisedPort; p != nil {
|
|
h.Config.APIOptions.APIPort = *p
|
|
}
|
|
h.Config.Mode.Store(configuration.ModeCluster)
|
|
h.Config.Cluster.BootstrapKey.Store(params.Data.BootstrapKey)
|
|
h.Config.Cluster.Clear()
|
|
// ensuring configuration file saving occurs before notifying the monitor about the bootstrap key change
|
|
defer func() {
|
|
h.Config.Notify.BootstrapKeyChanged.Notify()
|
|
}()
|
|
}
|
|
err := h.Config.Save()
|
|
if err != nil {
|
|
return h.err500(err, nil)
|
|
}
|
|
return cluster.NewPostClusterOK().WithPayload(getClusterSettings(h.Config))
|
|
}
|
|
|
|
// Handle executing the request and returning a response
|
|
func (h *GetClusterHandlerImpl) Handle(params cluster.GetClusterParams, principal interface{}) middleware.Responder {
|
|
return cluster.NewGetClusterOK().WithPayload(getClusterSettings(h.Config))
|
|
}
|
|
|
|
func (h *DeleteClusterHandlerImpl) Handle(params cluster.DeleteClusterParams, principal interface{}) middleware.Responder {
|
|
log.Warningf("received instructions from %s to switch to standalone mode", params.HTTPRequest.RemoteAddr)
|
|
// Only do when dataplane is in cluster mode, if not, do nothing and return 204
|
|
if h.Config.Mode.Load() == configuration.ModeCluster {
|
|
log.Warning("clearing cluster users")
|
|
for _, u := range h.Users.GetUsers() {
|
|
// remove all users for cluster communication
|
|
if strings.HasPrefix(u.Name, "dpapi-c-") {
|
|
errRU := h.Users.RemoveUser(u)
|
|
if errRU != nil {
|
|
log.Error(errRU.Error())
|
|
}
|
|
}
|
|
}
|
|
// If we don't want to keep the haproxy configuration, set it to dummy config
|
|
if params.Configuration == nil || *params.Configuration != "keep" {
|
|
log.Warning("clearing configuration as requested")
|
|
|
|
conf, err := h.Client.Configuration()
|
|
if err != nil {
|
|
return h.err500(err, nil)
|
|
}
|
|
version, errVersion := conf.GetVersion("")
|
|
if errVersion != nil || version < 1 {
|
|
// silently fallback to 1
|
|
version = 1
|
|
}
|
|
|
|
config := fmt.Sprintf(DummyConfig, time.Now().Format("01-02-2006 15:04:05 MST"), h.Config.Name.Load())
|
|
if err = conf.PostRawConfiguration(&config, version, true); err != nil {
|
|
return h.err500(err, nil)
|
|
}
|
|
|
|
// we need to restart haproxy
|
|
err = h.ReloadAgent.Restart()
|
|
if err != nil {
|
|
return h.err500(err, nil)
|
|
}
|
|
// Deleting the storage directory used by Fusion:
|
|
// avoiding at all entering any nil pointer dereference.
|
|
if storageData := h.Config.GetStorageData(); storageData != nil && storageData.Cluster != nil && storageData.Cluster.StorageDir != nil {
|
|
if storageErr := configuration.RemoveStorageFolder(*storageData.Cluster.StorageDir); storageErr != nil {
|
|
log.Warningf("failed to clean-up the cluster storage directory: %s", storageErr.Error())
|
|
}
|
|
}
|
|
}
|
|
h.Config.Cluster.BootstrapKey.Store("")
|
|
h.Config.Mode.Store(configuration.ModeSingle)
|
|
h.Config.Status.Store("active")
|
|
h.Config.Cluster.Clear()
|
|
defer func() {
|
|
log.Warning("reloading to apply configuration changes")
|
|
h.Config.Notify.Reload.Notify()
|
|
}()
|
|
}
|
|
err := h.Config.Save()
|
|
if err != nil {
|
|
return h.err500(err, nil)
|
|
}
|
|
return cluster.NewDeleteClusterNoContent()
|
|
}
|
|
|
|
func (h *DeleteClusterHandlerImpl) err500(err error, transaction *models.Transaction) middleware.Responder {
|
|
if transaction != nil {
|
|
configuration, confErr := h.Client.Configuration()
|
|
if confErr == nil {
|
|
_ = configuration.DeleteTransaction(transaction.ID)
|
|
}
|
|
}
|
|
msg := err.Error()
|
|
code := int64(500)
|
|
return cluster.NewDeleteClusterDefault(500).WithPayload(&models.Error{
|
|
Code: &code,
|
|
Message: &msg,
|
|
})
|
|
}
|
|
|
|
func (h *EditClusterHandlerImpl) Handle(params cluster.EditClusterParams, principal interface{}) middleware.Responder {
|
|
// Only do when dataplane is in cluster mode, if not, do nothing and return 204
|
|
if h.Config.Mode.Load() == configuration.ModeCluster {
|
|
// for now change only cluster log targets in PUT method
|
|
if params.Data != nil && params.Data.Cluster != nil {
|
|
if clusterLogTargetsChanged(h.Config.Cluster.ClusterLogTargets, params.Data.Cluster.ClusterLogTargets) {
|
|
h.Config.Cluster.ClusterLogTargets = params.Data.Cluster.ClusterLogTargets
|
|
h.Config.Cluster.ClusterID.Store(params.Data.Cluster.ClusterID)
|
|
err := h.Config.Save()
|
|
if err != nil {
|
|
return h.err500(err)
|
|
}
|
|
defer h.Config.Notify.Reload.Notify()
|
|
}
|
|
}
|
|
return cluster.NewEditClusterOK().WithPayload(getClusterSettings(h.Config))
|
|
}
|
|
return h.err406(fmt.Errorf("dataplaneapi in single mode"))
|
|
}
|
|
|
|
func (h *EditClusterHandlerImpl) err406(err error) middleware.Responder {
|
|
// 406 Not Acceptable
|
|
msg := err.Error()
|
|
code := int64(406)
|
|
return cluster.NewEditClusterDefault(406).WithPayload(&models.Error{
|
|
Code: &code,
|
|
Message: &msg,
|
|
})
|
|
}
|
|
|
|
func (h *EditClusterHandlerImpl) err500(err error) middleware.Responder {
|
|
msg := err.Error()
|
|
code := int64(500)
|
|
return cluster.NewEditClusterDefault(500).WithPayload(&models.Error{
|
|
Code: &code,
|
|
Message: &msg,
|
|
})
|
|
}
|
|
|
|
func getClusterSettings(cfg *configuration.Configuration) *models.ClusterSettings {
|
|
portStr := cfg.Cluster.Port.Load()
|
|
port := int64(portStr)
|
|
var clusterSettings *models.ClusterSettingsCluster
|
|
if cfg.Mode.Load() == configuration.ModeCluster {
|
|
clusterSettings = &models.ClusterSettingsCluster{
|
|
Address: cfg.Cluster.URL.Load(),
|
|
Port: &port,
|
|
APIBasePath: cfg.Cluster.APIBasePath.Load(),
|
|
Name: cfg.Cluster.Name.Load(),
|
|
Description: cfg.Cluster.Description.Load(),
|
|
ClusterLogTargets: cfg.Cluster.ClusterLogTargets,
|
|
}
|
|
}
|
|
settings := &models.ClusterSettings{
|
|
BootstrapKey: cfg.Cluster.BootstrapKey.Load(),
|
|
Cluster: clusterSettings,
|
|
Mode: cfg.Mode.Load(),
|
|
Status: cfg.Status.Load(),
|
|
}
|
|
return settings
|
|
}
|
|
|
|
func clusterLogTargetsChanged(oldCLT []*models.ClusterLogTarget, newCLT []*models.ClusterLogTarget) bool {
|
|
if len(oldCLT) == len(newCLT) {
|
|
eqCtr := 0
|
|
for _, oldT := range oldCLT {
|
|
for _, newT := range newCLT {
|
|
if reflect.DeepEqual(oldT, newT) {
|
|
eqCtr++
|
|
}
|
|
}
|
|
}
|
|
return !(eqCtr == len(oldCLT))
|
|
}
|
|
return true
|
|
}
|
|
|
|
const DummyConfig = `# NOTE: This configuration file was managed by the Fusion Control Plane.
|
|
# Fusion released the control at %s
|
|
|
|
defaults
|
|
mode http
|
|
timeout connect 5000
|
|
timeout client 30000
|
|
timeout server 10000
|
|
|
|
frontend disabled
|
|
bind /tmp/dataplaneapi-%s.sock name tmp
|
|
|
|
`
|
|
|