package config import ( "errors" "fmt" "net" "os" "path/filepath" "strings" "golang.org/x/crypto/bcrypt" "gopkg.in/yaml.v2" ) const ( RaftDir = "raft" RaftListen = ":7001" RaftLogLevel = "off" EtcdLogLevel = "error" EtcdPrefix = "/haproxy-lb" HAProxyService = "haproxy" HAProxyConfigFile = "/etc/haproxy/haproxy.cfg" ) func ParseFile(file string) (cfg *Config, err error) { if file == "" { return nil, errors.New("missing config file") } f, err := os.Open(file) if err != nil { return } defer f.Close() cfg = new(Config) err = yaml.NewDecoder(f).Decode(cfg) if err != nil { return } err = Validate(cfg) return } func Validate(c *Config) error { if c.HAProxyConfig == "" { c.HAProxyConfig = HAProxyConfigFile } if c.HAProxyService == "" { c.HAProxyService = HAProxyService } failover := c.Failover if failover != nil { if failover.IP == "" { return errors.New("missing failover.ip config") } _, _, err := net.ParseCIDR(failover.IP) if err != nil { return fmt.Errorf("failed to parse failover.ip: %w", err) } } if c.LeaderHook != "" { if !filepath.IsAbs(c.LeaderHook) { return errors.New("leaderHook path must be absolute") } } if c.FollowerHook != "" { if !filepath.IsAbs(c.FollowerHook) { return errors.New("followerHook path must be absolute") } } vip := c.VIP if vip.Prefix == "" { return errors.New("missing vip.prefix config") } local := vip.Store.Local netbox := vip.Store.Netbox if local == nil && netbox == nil { return errors.New("missing vip.store config: local or netbox") } if local != nil && netbox != nil { return errors.New("only one vip.store config allowed: local or netbox") } if netbox != nil { if netbox.Endpoint == "" { return errors.New("missing vip.store.netbox.endpoint config") } if netbox.Token == "" { return errors.New("missing vip.store.netbox.token config") } } raft := c.Cluster.Raft if raft != nil { if raft.LogLevel == "" { raft.LogLevel = RaftLogLevel } if raft.Dir == "" { raft.Dir = RaftDir } if raft.ID == "" { return errors.New("missing raft.id config") } if raft.Listen == "" { raft.Listen = RaftListen } if len(raft.Peers) < 2 { return errors.New("minimum number of remote peers: 2") } for i, v := range raft.Peers { if v.ID == "" { return fmt.Errorf("missing raft.peers[%d].id config", i) } if v.Address == "" { return fmt.Errorf("missing raft.peers[%d].address config", i) } } } etcd := c.Cluster.Etcd if etcd != nil { if etcd.LogLevel == "" { etcd.LogLevel = EtcdLogLevel } if len(etcd.Endpoints) == 0 { return errors.New("missing etcd.endpoints config") } if etcd.Prefix == "" { etcd.Prefix = EtcdPrefix } else { if !strings.HasPrefix(etcd.Prefix, "/") { etcd.Prefix = "/" + etcd.Prefix } if strings.HasSuffix(etcd.Prefix, "/") { etcd.Prefix = strings.TrimSuffix(etcd.Prefix, "/") } } if etcd.ClusterName == "" { return errors.New("missing etcd.clusterName config") } else { if strings.HasSuffix(etcd.ClusterName, "/") { etcd.ClusterName = strings.TrimSuffix(etcd.ClusterName, "/") } } } if raft == nil && etcd == nil { return errors.New("missing cluster config: raft or etcd") } if raft != nil && etcd != nil { return errors.New("only one cluster config allowed: raft or etcd") } if len(c.APIServer.BasicAuth) == 0 { return errors.New("missing basic auth configuration for api users") } for _, u := range c.APIServer.BasicAuth { if u.User == "" { return errors.New("basic auth username can not be empty") } _, err := bcrypt.Cost([]byte(u.Hash)) if u.Hash == "" { return fmt.Errorf("invalid basic auth hash for user %s: %w", u.User, err) } } return nil } type Config struct { LeaderHook string `yaml:"leaderHook,omitempty"` FollowerHook string `yaml:"followerHook,omitempty"` HAProxyConfig string `yaml:"haproxyConfig,omitempty"` HAProxyService string `yaml:"haproxyService,omitempty"` Failover *Failover `yaml:"failover"` VIP VIP `yaml:"vip"` Cluster Cluster `yaml:"cluster"` APIServer APIServer `yaml:"apiServer"` } type Failover struct { IP string `yaml:"ip"` Interface string `yaml:"interface"` } type VIP struct { Prefix string `yaml:"prefix"` Store Store `yaml:"store"` Interface string `yaml:"interface,omitempty"` Label string `yaml:"label,omitempty"` } type Store struct { Local *struct{} `yaml:"local"` Netbox *Netbox `yaml:"netbox"` } type Netbox struct { Endpoint string `yaml:"endpoint"` Token string `yaml:"token"` Insecure bool `yaml:"insecure,omitempty"` } type Cluster struct { Raft *Raft `yaml:"raft,omitempty"` Etcd *Etcd `yaml:"etcd,omitempty"` } type Raft struct { Dir string `yaml:"dir,omitempty"` ID string `yaml:"id"` Listen string `yaml:"listen"` Peers []RaftPeer `yaml:"peers"` TLS *TLS `yaml:"tls,omitempty"` LogLevel string `yaml:"logLevel,omitempty"` } type TLS struct { Cert string `yaml:"cert,omitempty"` Key string `yaml:"key,omitempty"` CA string `yaml:"ca,omitempty"` Insecure bool `yaml:"insecure,omitempty"` } // RaftPeer details the configuration of all cluster peers type RaftPeer struct { ID string `yaml:"id"` Address string `yaml:"address"` } type Etcd struct { Endpoints []string `yaml:"endpoints"` ClusterName string `yaml:"clusterName"` Prefix string `yaml:"prefix,omitempty"` Username string `yaml:"username,omitempty"` Password string `yaml:"password,omitempty"` TLS *TLS `yaml:"tls,omitempty"` LogLevel string `yaml:"logLevel,omitempty"` } type APIServer struct { Listen string `yaml:"listen"` TLS *TLS `yaml:"tls"` BasicAuth []BasicAuth `yaml:"basicAuth"` } type BasicAuth struct { User string `yaml:"user"` Hash string `yaml:"hash"` Prefix string `yaml:"prefix"` }