mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
230 lines
6.5 KiB
Go
230 lines
6.5 KiB
Go
// Package redis_policy_set provides a Redis implementation of service.FailingPolicySet.
|
|
package redis_policy_set
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/datastore/redis"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
redigo "github.com/gomodule/redigo/redis"
|
|
)
|
|
|
|
type redisFailingPolicySet struct {
|
|
pool fleet.RedisPool
|
|
testPrefix string // for tests, the key prefix to use to avoid conflicts
|
|
}
|
|
|
|
var _ fleet.FailingPolicySet = (*redisFailingPolicySet)(nil)
|
|
|
|
// NewFailing creates a redis policy set for failing policies.
|
|
func NewFailing(pool fleet.RedisPool) *redisFailingPolicySet {
|
|
return &redisFailingPolicySet{
|
|
pool: pool,
|
|
}
|
|
}
|
|
|
|
type TestNamer interface {
|
|
Name() string
|
|
}
|
|
|
|
// NewFailingTest creates a redis policy set for failing policies to be used
|
|
// only in tests.
|
|
func NewFailingTest(t TestNamer, pool fleet.RedisPool) *redisFailingPolicySet {
|
|
return &redisFailingPolicySet{
|
|
pool: pool,
|
|
testPrefix: t.Name() + ":",
|
|
}
|
|
}
|
|
|
|
const (
|
|
policySetKeyPrefix = "policies:failing:"
|
|
// policySetsSetKey is used to avoid a SCAN command when listing policy sets.
|
|
policySetsSetKey = "policies:failing_sets"
|
|
)
|
|
|
|
// ListSets lists all the policy sets.
|
|
func (r *redisFailingPolicySet) ListSets() ([]uint, error) {
|
|
conn := redis.ConfigureDoer(r.pool, r.pool.Get())
|
|
defer conn.Close()
|
|
|
|
ids, err := redigo.Ints(conn.Do("SMEMBERS", r.policySetOfSetsKey()))
|
|
if err != nil && err != redigo.ErrNil {
|
|
return nil, err
|
|
}
|
|
policyIDs := make([]uint, len(ids))
|
|
for i := range ids {
|
|
policyIDs[i] = uint(ids[i])
|
|
}
|
|
return policyIDs, nil
|
|
}
|
|
|
|
// AddHost adds the given host to the policy sets.
|
|
func (r *redisFailingPolicySet) AddHost(policyID uint, host fleet.PolicySetHost) error {
|
|
// The order of the following two operations is important.
|
|
//
|
|
// The ordering of operations in AddHost and RemoveSet has been chosen to avoid
|
|
// ending up with a policySetKey with a host entry, but without its corresponding entry
|
|
// in the set of sets.
|
|
//
|
|
// The commands are run in different functions with different connections in
|
|
// case it is running in redis cluster mode - the keys may not live on the
|
|
// same node. There is no additional connection cost for standalone mode, as
|
|
// the second connection will come from the pool (same for cluster mode if
|
|
// both key happen to live on the same node).
|
|
if err := r.addHostToPolicySet(policyID, host); err != nil {
|
|
return err
|
|
}
|
|
if err := r.addPolicyToSetOfSets(policyID); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// scanPolicySet uses SSCAN (instead of SMEMBERS) to fetch the hosts from a policy set with a cursor.
|
|
//
|
|
// SMEMBERS blocks the Redis instance for the duration of the call, so if the set is big
|
|
// it could impact performance overall.
|
|
func (r *redisFailingPolicySet) scanPolicySet(policyID uint) ([]string, error) {
|
|
const hostsScanCount = 100
|
|
var hosts []string
|
|
|
|
conn := redis.ConfigureDoer(r.pool, r.pool.Get())
|
|
defer conn.Close()
|
|
|
|
cursor := 0
|
|
for {
|
|
res, err := redigo.Values(conn.Do("SSCAN", r.policySetKey(policyID), cursor, "COUNT", hostsScanCount))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan keys: %w", err)
|
|
}
|
|
var curElems []string
|
|
_, err = redigo.Scan(res, &cursor, &curElems)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("convert scan results: %w", err)
|
|
}
|
|
hosts = append(hosts, curElems...)
|
|
if cursor == 0 {
|
|
break
|
|
}
|
|
}
|
|
return hosts, nil
|
|
}
|
|
|
|
// ListHosts returns the list of hosts present in the policy set.
|
|
func (r *redisFailingPolicySet) ListHosts(policyID uint) ([]fleet.PolicySetHost, error) {
|
|
hostEntries, err := r.scanPolicySet(policyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hosts := make([]fleet.PolicySetHost, len(hostEntries))
|
|
for i := range hostEntries {
|
|
policySetHost, err := parseHostEntry(hostEntries[i])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse host entry: %w", err)
|
|
}
|
|
hosts[i] = *policySetHost
|
|
}
|
|
return hosts, nil
|
|
}
|
|
|
|
// RemoveHosts removes the hosts from the policy set.
|
|
func (r *redisFailingPolicySet) RemoveHosts(policyID uint, hosts []fleet.PolicySetHost) error {
|
|
if len(hosts) == 0 {
|
|
return nil
|
|
}
|
|
|
|
conn := redis.ConfigureDoer(r.pool, r.pool.Get())
|
|
defer conn.Close()
|
|
|
|
var args redigo.Args
|
|
args = args.Add(r.policySetKey(policyID))
|
|
for _, host := range hosts {
|
|
args = args.Add(hostEntry(host))
|
|
}
|
|
_, err := conn.Do("SREM", args...)
|
|
return err
|
|
}
|
|
|
|
// RemoveSet removes a policy set.
|
|
func (r *redisFailingPolicySet) RemoveSet(policyID uint) error {
|
|
// The order of the following two operations is important.
|
|
//
|
|
// See comment in AddHost.
|
|
if err := r.removePolicyFromSetOfSets(policyID); err != nil {
|
|
return err
|
|
}
|
|
if err := r.removePolicySet(policyID); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *redisFailingPolicySet) addHostToPolicySet(policyID uint, host fleet.PolicySetHost) error {
|
|
conn := redis.ConfigureDoer(r.pool, r.pool.Get())
|
|
defer conn.Close()
|
|
|
|
if _, err := conn.Do("SADD", r.policySetKey(policyID), hostEntry(host)); err != nil {
|
|
return fmt.Errorf("add host entry to policy set: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *redisFailingPolicySet) removePolicySet(policyID uint) error {
|
|
conn := redis.ConfigureDoer(r.pool, r.pool.Get())
|
|
defer conn.Close()
|
|
|
|
if _, err := conn.Do("DEL", r.policySetKey(policyID)); err != nil {
|
|
return fmt.Errorf("remove policy set: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *redisFailingPolicySet) addPolicyToSetOfSets(policyID uint) error {
|
|
conn := redis.ConfigureDoer(r.pool, r.pool.Get())
|
|
defer conn.Close()
|
|
|
|
if _, err := conn.Do("SADD", r.policySetOfSetsKey(), policyID); err != nil {
|
|
return fmt.Errorf("add policy id to set of failing sets: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *redisFailingPolicySet) removePolicyFromSetOfSets(policyID uint) error {
|
|
conn := redis.ConfigureDoer(r.pool, r.pool.Get())
|
|
defer conn.Close()
|
|
|
|
if _, err := conn.Do("SREM", r.policySetOfSetsKey(), policyID); err != nil {
|
|
return fmt.Errorf("remove policy id from set of failing sets: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *redisFailingPolicySet) policySetKey(policyID uint) string {
|
|
return r.testPrefix + policySetKeyPrefix + strconv.Itoa(int(policyID))
|
|
}
|
|
|
|
func (r *redisFailingPolicySet) policySetOfSetsKey() string {
|
|
return r.testPrefix + policySetsSetKey
|
|
}
|
|
|
|
func hostEntry(host fleet.PolicySetHost) string {
|
|
return strconv.Itoa(int(host.ID)) + "," + host.Hostname
|
|
}
|
|
|
|
func parseHostEntry(v string) (*fleet.PolicySetHost, error) {
|
|
parts := strings.SplitN(v, ",", 2)
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf("invalid format: %s", v)
|
|
}
|
|
id, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid id: %s", v)
|
|
}
|
|
return &fleet.PolicySetHost{
|
|
ID: uint(id),
|
|
Hostname: parts[1],
|
|
}, nil
|
|
}
|