storj/satellite/overlay/placement.go

165 lines
5.4 KiB
Go
Raw Normal View History

// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
package overlay
import (
"fmt"
"strconv"
"strings"
"github.com/jtolio/mito"
"github.com/spf13/pflag"
"github.com/zeebo/errs"
"storj.io/common/storj"
"storj.io/common/storj/location"
"storj.io/storj/satellite/nodeselection"
)
// PlacementRules can crate filter based on the placement identifier.
type PlacementRules func(constraint storj.PlacementConstraint) (filter nodeselection.NodeFilter)
// ConfigurablePlacementRule can include the placement definitions for each known identifier.
type ConfigurablePlacementRule struct {
placements map[storj.PlacementConstraint]nodeselection.NodeFilter
}
// String implements pflag.Value.
func (d *ConfigurablePlacementRule) String() string {
parts := []string{}
for id, filter := range d.placements {
// we can hide the internal rules...
if id > 9 {
// TODO: we need proper String implementation for all the used filters
parts = append(parts, fmt.Sprintf("%d:%s", id, filter))
}
}
return strings.Join(parts, ";")
}
// Set implements pflag.Value.
func (d *ConfigurablePlacementRule) Set(s string) error {
if d.placements == nil {
d.placements = map[storj.PlacementConstraint]nodeselection.NodeFilter{
storj.EveryCountry: nodeselection.AnyFilter{},
}
}
d.AddLegacyStaticRules()
return d.AddPlacementFromString(s)
}
// Type implements pflag.Value.
func (d *ConfigurablePlacementRule) Type() string {
return "placement-rule"
}
var _ pflag.Value = &ConfigurablePlacementRule{}
// NewPlacementRules creates a fully initialized NewPlacementRules.
func NewPlacementRules() *ConfigurablePlacementRule {
return &ConfigurablePlacementRule{
placements: map[storj.PlacementConstraint]nodeselection.NodeFilter{
storj.EveryCountry: nodeselection.AnyFilter{}},
}
}
// AddLegacyStaticRules initializes all the placement rules defined earlier in static golang code.
func (d *ConfigurablePlacementRule) AddLegacyStaticRules() {
d.placements[storj.EEA] = nodeselection.NodeFilters{nodeselection.NewCountryFilter(nodeselection.EeaCountries)}
d.placements[storj.EU] = nodeselection.NodeFilters{nodeselection.NewCountryFilter(nodeselection.EuCountries)}
d.placements[storj.US] = nodeselection.NodeFilters{nodeselection.NewCountryFilter(location.NewSet(location.UnitedStates))}
d.placements[storj.DE] = nodeselection.NodeFilters{nodeselection.NewCountryFilter(location.NewSet(location.Germany))}
d.placements[storj.NR] = nodeselection.NodeFilters{nodeselection.NewCountryFilter(location.NewFullSet().Without(location.Russia, location.Belarus, location.None))}
}
// AddPlacementRule registers a new placement.
func (d *ConfigurablePlacementRule) AddPlacementRule(id storj.PlacementConstraint, filter nodeselection.NodeFilter) {
d.placements[id] = filter
}
// AddPlacementFromString parses placement definition form string representations from id:definition;id:definition;...
func (d *ConfigurablePlacementRule) AddPlacementFromString(definitions string) error {
env := map[any]any{
"country": func(countries ...string) (nodeselection.NodeFilters, error) {
var set location.Set
for _, country := range countries {
code := location.ToCountryCode(country)
if code == location.None {
return nil, errs.New("invalid country code %q", code)
}
set.Include(code)
}
return nodeselection.NodeFilters{nodeselection.NewCountryFilter(set)}, nil
},
"all": func(filters ...nodeselection.NodeFilters) (nodeselection.NodeFilters, error) {
res := nodeselection.NodeFilters{}
for _, filter := range filters {
res = append(res, filter...)
}
return res, nil
},
"tag": func(nodeIDstr string, key string, value any) (nodeselection.NodeFilters, error) {
nodeID, err := storj.NodeIDFromString(nodeIDstr)
if err != nil {
return nil, err
}
var rawValue []byte
switch v := value.(type) {
case string:
rawValue = []byte(v)
case []byte:
rawValue = v
default:
return nil, errs.New("3rd argument of tag() should be string or []byte")
}
res := nodeselection.NodeFilters{
nodeselection.NewTagFilter(nodeID, key, rawValue),
}
return res, nil
},
"annotated": func(filter nodeselection.NodeFilter, kv map[string]string) (nodeselection.AnnotatedNodeFilter, error) {
return nodeselection.AnnotatedNodeFilter{
Filter: filter,
Annotations: kv,
}, nil
},
"annotation": func(key string, value string) (map[string]string, error) {
return map[string]string{
key: value,
}, nil
},
"exclude": func(filter nodeselection.NodeFilter) (nodeselection.NodeFilter, error) {
return nodeselection.NewExcludeFilter(filter), nil
},
}
for _, definition := range strings.Split(definitions, ";") {
definition = strings.TrimSpace(definition)
if definition == "" {
continue
}
idDef := strings.SplitN(definition, ":", 2)
val, err := mito.Eval(idDef[1], env)
if err != nil {
return errs.Wrap(err)
}
id, err := strconv.Atoi(idDef[0])
if err != nil {
return errs.Wrap(err)
}
d.placements[storj.PlacementConstraint(id)] = val.(nodeselection.NodeFilter)
}
return nil
}
// CreateFilters implements PlacementCondition.
func (d *ConfigurablePlacementRule) CreateFilters(constraint storj.PlacementConstraint) (filter nodeselection.NodeFilter) {
if filters, found := d.placements[constraint]; found {
return filters
}
return nodeselection.NodeFilters{
nodeselection.ExcludeAllFilter{},
}
}