Fix gathering of hostnames, add /v3/cluster/db/kv endpoint

This commit is contained in:
Ingo Oppermann
2023-06-27 14:52:32 +02:00
parent 4560b1a6ca
commit dd128ac99b
15 changed files with 234 additions and 52 deletions

View File

@@ -433,32 +433,6 @@ func (a *api) start(ctx context.Context) error {
} }
if cfg.Cluster.Enable { if cfg.Cluster.Enable {
scheme := "http://"
address := cfg.Address
if cfg.TLS.Enable {
scheme = "https://"
address = cfg.TLS.Address
}
host, port, err := gonet.SplitHostPort(address)
if err != nil {
return fmt.Errorf("invalid core address: %s: %w", address, err)
}
if len(host) == 0 {
chost, _, err := gonet.SplitHostPort(cfg.Cluster.Address)
if err != nil {
return fmt.Errorf("invalid cluster address: %s: %w", cfg.Cluster.Address, err)
}
if len(chost) == 0 {
return fmt.Errorf("invalid cluster address: %s: %w", cfg.Cluster.Address, err)
}
host = chost
}
peers := []cluster.Peer{} peers := []cluster.Peer{}
for _, p := range cfg.Cluster.Peers { for _, p := range cfg.Cluster.Peers {
@@ -486,9 +460,6 @@ func (a *api) start(ctx context.Context) error {
NodeRecoverTimeout: time.Duration(cfg.Cluster.NodeRecoverTimeout) * time.Second, NodeRecoverTimeout: time.Duration(cfg.Cluster.NodeRecoverTimeout) * time.Second,
EmergencyLeaderTimeout: time.Duration(cfg.Cluster.EmergencyLeaderTimeout) * time.Second, EmergencyLeaderTimeout: time.Duration(cfg.Cluster.EmergencyLeaderTimeout) * time.Second,
CoreConfig: cfg.Clone(), CoreConfig: cfg.Clone(),
CoreAPIAddress: scheme + gonet.JoinHostPort(host, port),
CoreAPIUsername: cfg.API.Auth.Username,
CoreAPIPassword: cfg.API.Auth.Password,
IPLimiter: a.sessionsLimiter, IPLimiter: a.sessionsLimiter,
Logger: a.log.logger.core.WithComponent("Cluster"), Logger: a.log.logger.core.WithComponent("Cluster"),
}) })
@@ -531,7 +502,7 @@ func (a *api) start(ctx context.Context) error {
a.log.logger.core.Error().WithError(err).Log("Failed to acquire certificates") a.log.logger.core.Error().WithError(err).Log("Failed to acquire certificates")
} }
if err == nil { if err != nil {
a.log.logger.core.Warn().Log("Continuing with disabled TLS") a.log.logger.core.Warn().Log("Continuing with disabled TLS")
autocertManager = nil autocertManager = nil
cfg.TLS.Enable = false cfg.TLS.Enable = false

View File

@@ -99,10 +99,6 @@ type Config struct {
NodeRecoverTimeout time.Duration // Timeout for a node to recover before rebalancing the processes NodeRecoverTimeout time.Duration // Timeout for a node to recover before rebalancing the processes
EmergencyLeaderTimeout time.Duration // Timeout for establishing the emergency leadership after lost contact to raft leader EmergencyLeaderTimeout time.Duration // Timeout for establishing the emergency leadership after lost contact to raft leader
CoreAPIAddress string // Address of the core API
CoreAPIUsername string // Username for the core API
CoreAPIPassword string // Password for the core API
CoreConfig *config.Config CoreConfig *config.Config
IPLimiter net.IPLimiter IPLimiter net.IPLimiter
@@ -180,6 +176,12 @@ func New(ctx context.Context, config Config) (Cluster, error) {
nodeRecoverTimeout: config.NodeRecoverTimeout, nodeRecoverTimeout: config.NodeRecoverTimeout,
emergencyLeaderTimeout: config.EmergencyLeaderTimeout, emergencyLeaderTimeout: config.EmergencyLeaderTimeout,
isDegraded: true,
isDegradedErr: fmt.Errorf("cluster not yet startet"),
isCoreDegraded: true,
isCoreDegradedErr: fmt.Errorf("cluster not yet started"),
config: config.CoreConfig, config: config.CoreConfig,
nodes: map[string]*clusterNode{}, nodes: map[string]*clusterNode{},
} }
@@ -190,15 +192,34 @@ func New(ctx context.Context, config Config) (Cluster, error) {
c.isTLSRequired = c.config.TLS.Enable && c.config.TLS.Auto c.isTLSRequired = c.config.TLS.Enable && c.config.TLS.Auto
u, err := url.Parse(config.CoreAPIAddress) host, port, err := gonet.SplitHostPort(c.config.Address)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid core API address: %w", err) return nil, fmt.Errorf("invalid core address: %s: %w", c.config.Address, err)
} }
if len(config.CoreAPIPassword) == 0 { chost, _, err := gonet.SplitHostPort(c.config.Cluster.Address)
u.User = url.User(config.CoreAPIUsername) if err != nil {
return nil, fmt.Errorf("invalid cluster address: %s: %w", c.config.Cluster.Address, err)
}
if len(chost) == 0 {
return nil, fmt.Errorf("invalid cluster address: %s: %w", c.config.Cluster.Address, err)
}
if len(host) == 0 {
host = chost
}
u := &url.URL{
Scheme: "http",
Host: gonet.JoinHostPort(host, port),
Path: "/",
}
if len(c.config.API.Auth.Password) == 0 {
u.User = url.User(c.config.API.Auth.Username)
} else { } else {
u.User = url.UserPassword(config.CoreAPIUsername, config.CoreAPIPassword) u.User = url.UserPassword(c.config.API.Auth.Username, c.config.API.Auth.Password)
} }
c.coreAddress = u.String() c.coreAddress = u.String()
@@ -375,19 +396,24 @@ func New(ctx context.Context, config Config) (Cluster, error) {
names, err := c.getClusterHostnames() names, err := c.getClusterHostnames()
if err != nil { if err != nil {
c.Shutdown() c.Shutdown()
return nil, fmt.Errorf("failed to assemble list of all configured hostnames: %w", err) return nil, fmt.Errorf("tls: failed to assemble list of all configured hostnames: %w", err)
} }
kvs, err := NewClusterKVS(c) if len(names) == 0 {
c.Shutdown()
return nil, fmt.Errorf("tls: no hostnames are configured")
}
kvs, err := NewClusterKVS(c, c.logger.WithComponent("KVS"))
if err != nil { if err != nil {
c.Shutdown() c.Shutdown()
return nil, fmt.Errorf("cluster KVS: %w", err) return nil, fmt.Errorf("tls: cluster KVS: %w", err)
} }
storage, err := NewClusterStorage(kvs, "core-cluster-certificates") storage, err := NewClusterStorage(kvs, "core-cluster-certificates")
if err != nil { if err != nil {
c.Shutdown() c.Shutdown()
return nil, fmt.Errorf("certificate store: %w", err) return nil, fmt.Errorf("tls: certificate store: %w", err)
} }
manager, err := autocert.New(autocert.Config{ manager, err := autocert.New(autocert.Config{
@@ -399,7 +425,7 @@ func New(ctx context.Context, config Config) (Cluster, error) {
}) })
if err != nil { if err != nil {
c.Shutdown() c.Shutdown()
return nil, fmt.Errorf("certificate manager: %w", err) return nil, fmt.Errorf("tls: certificate manager: %w", err)
} }
c.certManager = manager c.certManager = manager
@@ -407,7 +433,7 @@ func New(ctx context.Context, config Config) (Cluster, error) {
err = manager.AcquireCertificates(ctx, c.config.Address, names) err = manager.AcquireCertificates(ctx, c.config.Address, names)
if err != nil { if err != nil {
c.Shutdown() c.Shutdown()
return nil, fmt.Errorf("failed to acquire certificates: %w", err) return nil, fmt.Errorf("tls: failed to acquire certificates: %w", err)
} }
} }
} }
@@ -585,9 +611,27 @@ func (c *cluster) IsDegraded() (bool, error) {
func (c *cluster) IsClusterDegraded() (bool, error) { func (c *cluster) IsClusterDegraded() (bool, error) {
c.stateLock.Lock() c.stateLock.Lock()
defer c.stateLock.Unlock() isDegraded, isDegradedErr := c.isDegraded, c.isDegradedErr
c.stateLock.Unlock()
return c.isDegraded, c.isDegradedErr if isDegraded {
return isDegraded, isDegradedErr
}
servers, err := c.raft.Servers()
if err != nil {
return true, err
}
c.nodesLock.Lock()
nodes := len(c.nodes)
c.nodesLock.Unlock()
if len(servers) != nodes {
return true, fmt.Errorf("not all nodes are connected")
}
return false, nil
} }
func (c *cluster) Leave(origin, id string) error { func (c *cluster) Leave(origin, id string) error {

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/datarhei/core/v16/cluster/store" "github.com/datarhei/core/v16/cluster/store"
"github.com/datarhei/core/v16/log"
) )
type KVS interface { type KVS interface {
@@ -62,21 +63,32 @@ func (l *Lock) Unlock() {
type clusterKVS struct { type clusterKVS struct {
cluster Cluster cluster Cluster
logger log.Logger
} }
func NewClusterKVS(cluster Cluster) (KVS, error) { func NewClusterKVS(cluster Cluster, logger log.Logger) (KVS, error) {
s := &clusterKVS{ s := &clusterKVS{
cluster: cluster, cluster: cluster,
logger: logger,
}
if s.logger == nil {
s.logger = log.New("")
} }
return s, nil return s, nil
} }
func (s *clusterKVS) CreateLock(name string, validUntil time.Time) (*Lock, error) { func (s *clusterKVS) CreateLock(name string, validUntil time.Time) (*Lock, error) {
s.logger.Debug().WithFields(log.Fields{
"name": name,
"valid_until": validUntil,
}).Log("Create lock")
return s.cluster.CreateLock("", name, validUntil) return s.cluster.CreateLock("", name, validUntil)
} }
func (s *clusterKVS) DeleteLock(name string) error { func (s *clusterKVS) DeleteLock(name string) error {
s.logger.Debug().WithField("name", name).Log("Delete lock")
return s.cluster.DeleteLock("", name) return s.cluster.DeleteLock("", name)
} }
@@ -85,14 +97,20 @@ func (s *clusterKVS) ListLocks() map[string]time.Time {
} }
func (s *clusterKVS) SetKV(key, value string) error { func (s *clusterKVS) SetKV(key, value string) error {
s.logger.Debug().WithFields(log.Fields{
"key": key,
"value": value,
}).Log("Set KV")
return s.cluster.SetKV("", key, value) return s.cluster.SetKV("", key, value)
} }
func (s *clusterKVS) UnsetKV(key string) error { func (s *clusterKVS) UnsetKV(key string) error {
s.logger.Debug().WithField("key", key).Log("Unset KV")
return s.cluster.UnsetKV("", key) return s.cluster.UnsetKV("", key)
} }
func (s *clusterKVS) GetKV(key string) (string, time.Time, error) { func (s *clusterKVS) GetKV(key string) (string, time.Time, error) {
s.logger.Debug().WithField("key", key).Log("Get KV")
return s.cluster.GetKV(key) return s.cluster.GetKV(key)
} }

View File

@@ -233,7 +233,7 @@ func (n *node) connect(ctx context.Context) error {
} }
peer, err := client.New(client.Config{ peer, err := client.New(client.Config{
Address: n.address, Address: u.String(),
Client: &http.Client{ Client: &http.Client{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
}, },

View File

@@ -483,7 +483,7 @@ func (d *Config) Validate(resetLogs bool) {
} }
} }
// If cluster mode is enabled, we can't join and bootstrap at the same time // If cluster mode is enabled, a proper address must be provided
if d.Cluster.Enable { if d.Cluster.Enable {
if len(d.Cluster.Address) == 0 { if len(d.Cluster.Address) == 0 {
d.vars.Log("error", "cluster.address", "must be provided") d.vars.Log("error", "cluster.address", "must be provided")

View File

@@ -154,6 +154,32 @@ const docTemplate = `{
} }
} }
}, },
"/api/v3/cluster/db/kv": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "List of KV in the cluster DB",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "List KV in the cluster DB",
"operationId": "cluster-3-db-list-kv",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.ClusterKVS"
}
}
}
}
},
"/api/v3/cluster/db/locks": { "/api/v3/cluster/db/locks": {
"get": { "get": {
"security": [ "security": [
@@ -4174,6 +4200,23 @@ const docTemplate = `{
} }
} }
}, },
"api.ClusterKVS": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/api.ClusterKVSValue"
}
},
"api.ClusterKVSValue": {
"type": "object",
"properties": {
"updated_at": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"api.ClusterLock": { "api.ClusterLock": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -146,6 +146,32 @@
} }
} }
}, },
"/api/v3/cluster/db/kv": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "List of KV in the cluster DB",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "List KV in the cluster DB",
"operationId": "cluster-3-db-list-kv",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.ClusterKVS"
}
}
}
}
},
"/api/v3/cluster/db/locks": { "/api/v3/cluster/db/locks": {
"get": { "get": {
"security": [ "security": [
@@ -4166,6 +4192,23 @@
} }
} }
}, },
"api.ClusterKVS": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/api.ClusterKVSValue"
}
},
"api.ClusterKVSValue": {
"type": "object",
"properties": {
"updated_at": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"api.ClusterLock": { "api.ClusterLock": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -88,6 +88,17 @@ definitions:
version: version:
type: string type: string
type: object type: object
api.ClusterKVS:
additionalProperties:
$ref: '#/definitions/api.ClusterKVSValue'
type: object
api.ClusterKVSValue:
properties:
updated_at:
type: string
value:
type: string
type: object
api.ClusterLock: api.ClusterLock:
properties: properties:
name: name:
@@ -2467,6 +2478,22 @@ paths:
summary: List of nodes in the cluster summary: List of nodes in the cluster
tags: tags:
- v16.?.? - v16.?.?
/api/v3/cluster/db/kv:
get:
description: List of KV in the cluster DB
operationId: cluster-3-db-list-kv
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.ClusterKVS'
security:
- ApiKeyAuth: []
summary: List KV in the cluster DB
tags:
- v16.?.?
/api/v3/cluster/db/locks: /api/v3/cluster/db/locks:
get: get:
description: List of locks in the cluster DB description: List of locks in the cluster DB

2
go.mod
View File

@@ -9,7 +9,7 @@ require (
github.com/atrox/haikunatorgo/v2 v2.0.1 github.com/atrox/haikunatorgo/v2 v2.0.1
github.com/caddyserver/certmagic v0.18.0 github.com/caddyserver/certmagic v0.18.0
github.com/casbin/casbin/v2 v2.71.1 github.com/casbin/casbin/v2 v2.71.1
github.com/datarhei/core-client-go/v16 v16.11.1-0.20230621092648-5cc658d8a73c github.com/datarhei/core-client-go/v16 v16.11.1-0.20230627120001-16d06aa77802
github.com/datarhei/gosrt v0.5.2 github.com/datarhei/gosrt v0.5.2
github.com/datarhei/joy4 v0.0.0-20230505074825-fde05957445a github.com/datarhei/joy4 v0.0.0-20230505074825-fde05957445a
github.com/fujiwara/shapeio v1.0.0 github.com/fujiwara/shapeio v1.0.0

2
go.sum
View File

@@ -62,6 +62,8 @@ github.com/datarhei/core-client-go/v16 v16.11.1-0.20230621090732-c6ae9699cea6 h1
github.com/datarhei/core-client-go/v16 v16.11.1-0.20230621090732-c6ae9699cea6/go.mod h1:6L0zr/NUwvaPsCTK/IL17m8JUEtgLp3BDtlsBREwacg= github.com/datarhei/core-client-go/v16 v16.11.1-0.20230621090732-c6ae9699cea6/go.mod h1:6L0zr/NUwvaPsCTK/IL17m8JUEtgLp3BDtlsBREwacg=
github.com/datarhei/core-client-go/v16 v16.11.1-0.20230621092648-5cc658d8a73c h1:WUjP8x7hl1EsTbV5w8WEhK6c8t3uRqLUSn7+FFHDpJU= github.com/datarhei/core-client-go/v16 v16.11.1-0.20230621092648-5cc658d8a73c h1:WUjP8x7hl1EsTbV5w8WEhK6c8t3uRqLUSn7+FFHDpJU=
github.com/datarhei/core-client-go/v16 v16.11.1-0.20230621092648-5cc658d8a73c/go.mod h1:6L0zr/NUwvaPsCTK/IL17m8JUEtgLp3BDtlsBREwacg= github.com/datarhei/core-client-go/v16 v16.11.1-0.20230621092648-5cc658d8a73c/go.mod h1:6L0zr/NUwvaPsCTK/IL17m8JUEtgLp3BDtlsBREwacg=
github.com/datarhei/core-client-go/v16 v16.11.1-0.20230627120001-16d06aa77802 h1:F4ILviOV6brxg25hMyzzyI0K/C9yLzYHgecPD1PaokQ=
github.com/datarhei/core-client-go/v16 v16.11.1-0.20230627120001-16d06aa77802/go.mod h1:6L0zr/NUwvaPsCTK/IL17m8JUEtgLp3BDtlsBREwacg=
github.com/datarhei/gosrt v0.5.2 h1:eagqZwEIiGPNJW0rLep3gwceObyaZ17+iKRc+l4VEpc= github.com/datarhei/gosrt v0.5.2 h1:eagqZwEIiGPNJW0rLep3gwceObyaZ17+iKRc+l4VEpc=
github.com/datarhei/gosrt v0.5.2/go.mod h1:0308GQhAu5hxe2KYdbss901aKceSSKXnwCr8Vs++eiw= github.com/datarhei/gosrt v0.5.2/go.mod h1:0308GQhAu5hxe2KYdbss901aKceSSKXnwCr8Vs++eiw=
github.com/datarhei/joy4 v0.0.0-20230505074825-fde05957445a h1:Tf4DSHY1xruBglr+yYP5Wct7czM86GKMYgbXH8a7OFo= github.com/datarhei/joy4 v0.0.0-20230505074825-fde05957445a h1:Tf4DSHY1xruBglr+yYP5Wct7czM86GKMYgbXH8a7OFo=

View File

@@ -92,3 +92,10 @@ type ClusterLock struct {
Name string `json:"name"` Name string `json:"name"`
ValidUntil time.Time `json:"valid_until"` ValidUntil time.Time `json:"valid_until"`
} }
type ClusterKVSValue struct {
Value string `json:"value"`
UpdatedAt time.Time `json:"updated_at"`
}
type ClusterKVS map[string]ClusterKVSValue

View File

@@ -1267,3 +1267,27 @@ func (h *ClusterHandler) ListStoreLocks(c echo.Context) error {
return c.JSON(http.StatusOK, locks) return c.JSON(http.StatusOK, locks)
} }
// ListStoreKV returns the list of currently stored key/value pairs
// @Summary List KV in the cluster DB
// @Description List of KV in the cluster DB
// @Tags v16.?.?
// @ID cluster-3-db-list-kv
// @Produce json
// @Success 200 {object} api.ClusterKVS
// @Security ApiKeyAuth
// @Router /api/v3/cluster/db/kv [get]
func (h *ClusterHandler) ListStoreKV(c echo.Context) error {
clusterkv := h.cluster.ListKV("")
kvs := api.ClusterKVS{}
for key, v := range clusterkv {
kvs[key] = api.ClusterKVSValue{
Value: v.Value,
UpdatedAt: v.UpdatedAt,
}
}
return c.JSON(http.StatusOK, kvs)
}

View File

@@ -680,6 +680,7 @@ func (s *server) setRoutesV3(v3 *echo.Group) {
v3.GET("/cluster/db/user/:name", s.v3handler.cluster.ListStoreIdentity) v3.GET("/cluster/db/user/:name", s.v3handler.cluster.ListStoreIdentity)
v3.GET("/cluster/db/policies", s.v3handler.cluster.ListStorePolicies) v3.GET("/cluster/db/policies", s.v3handler.cluster.ListStorePolicies)
v3.GET("/cluster/db/locks", s.v3handler.cluster.ListStoreLocks) v3.GET("/cluster/db/locks", s.v3handler.cluster.ListStoreLocks)
v3.GET("/cluster/db/kv", s.v3handler.cluster.ListStoreKV)
v3.GET("/cluster/iam/user", s.v3handler.cluster.ListIdentities) v3.GET("/cluster/iam/user", s.v3handler.cluster.ListIdentities)
v3.GET("/cluster/iam/user/:name", s.v3handler.cluster.ListIdentity) v3.GET("/cluster/iam/user/:name", s.v3handler.cluster.ListIdentity)

View File

@@ -172,6 +172,8 @@ func New(config Config) (RestClient, error) {
r.address = u.String() r.address = u.String()
} }
r.address = strings.TrimSuffix(r.address, "/")
if r.client == nil { if r.client == nil {
r.client = &http.Client{ r.client = &http.Client{
Timeout: 15 * time.Second, Timeout: 15 * time.Second,

2
vendor/modules.txt vendored
View File

@@ -78,7 +78,7 @@ github.com/cespare/xxhash/v2
# github.com/cpuguy83/go-md2man/v2 v2.0.2 # github.com/cpuguy83/go-md2man/v2 v2.0.2
## explicit; go 1.11 ## explicit; go 1.11
github.com/cpuguy83/go-md2man/v2/md2man github.com/cpuguy83/go-md2man/v2/md2man
# github.com/datarhei/core-client-go/v16 v16.11.1-0.20230621092648-5cc658d8a73c # github.com/datarhei/core-client-go/v16 v16.11.1-0.20230627120001-16d06aa77802
## explicit; go 1.18 ## explicit; go 1.18
github.com/datarhei/core-client-go/v16 github.com/datarhei/core-client-go/v16
github.com/datarhei/core-client-go/v16/api github.com/datarhei/core-client-go/v16/api