orchestrator/clickhouse: use SubnetMap for parsing networks

This commit is contained in:
Vincent Bernat
2022-08-01 09:03:48 +02:00
parent c72f10583d
commit 5691b13050
5 changed files with 57 additions and 87 deletions

View File

@@ -33,6 +33,19 @@ func (sm *SubnetMap[V]) Lookup(ip net.IP) (V, bool) {
return value, ok return value, ok
} }
// ToMap return a map of the tree.
func (sm *SubnetMap[V]) ToMap() map[string]V {
output := map[string]V{}
if sm == nil || sm.tree == nil {
return output
}
iter := sm.tree.Iterate()
for iter.Next() {
output[iter.Address().String()] = iter.Tags()[0]
}
return output
}
// NewSubnetMap creates a subnetmap from a map. Unlike user-provided // NewSubnetMap creates a subnetmap from a map. Unlike user-provided
// configuration, this function is stricter and require everything to // configuration, this function is stricter and require everything to
// be IPv6 subnets. // be IPv6 subnets.
@@ -137,7 +150,7 @@ func SubnetMapUnmarshallerHook[V any]() mapstructure.DecodeHookFunc {
if err := intermediateDecoder.Decode(output); err != nil { if err := intermediateDecoder.Decode(output); err != nil {
return nil, fmt.Errorf("unable to decode %q: %w", reflect.TypeOf(zero).Name(), err) return nil, fmt.Errorf("unable to decode %q: %w", reflect.TypeOf(zero).Name(), err)
} }
trie, err := NewSubnetMap[V](intermediate) trie, err := NewSubnetMap(intermediate)
if err != nil { if err != nil {
// Should not happen // Should not happen
return nil, err return nil, err
@@ -148,21 +161,10 @@ func SubnetMapUnmarshallerHook[V any]() mapstructure.DecodeHookFunc {
} }
func (sm SubnetMap[V]) MarshalYAML() (interface{}, error) { func (sm SubnetMap[V]) MarshalYAML() (interface{}, error) {
output := map[string]V{} return sm.ToMap(), nil
if sm.tree == nil {
return output, nil
}
iter := sm.tree.Iterate()
for iter.Next() {
output[iter.Address().String()] = iter.Tags()[0]
}
return output, nil
} }
func (sm SubnetMap[V]) String() string { func (sm SubnetMap[V]) String() string {
out, err := sm.MarshalYAML() out := sm.ToMap()
if err != nil {
return "SubnetMap???"
}
return fmt.Sprintf("%v", out) return fmt.Sprintf("%v", out)
} }

View File

@@ -4,8 +4,6 @@
package clickhouse package clickhouse
import ( import (
"fmt"
"net"
"reflect" "reflect"
"time" "time"
@@ -32,7 +30,7 @@ type Configuration struct {
ASNs map[uint32]string ASNs map[uint32]string
// Networks is a mapping from IP networks to attributes. It is used // Networks is a mapping from IP networks to attributes. It is used
// to instantiate the SrcNet* and DstNet* columns. // to instantiate the SrcNet* and DstNet* columns.
Networks NetworkMap Networks *helpers.SubnetMap[NetworkAttributes]
// OrchestratorURL allows one to override URL to reach // OrchestratorURL allows one to override URL to reach
// orchestrator from Clickhouse // orchestrator from Clickhouse
OrchestratorURL string `validate:"isdefault|url"` OrchestratorURL string `validate:"isdefault|url"`
@@ -73,9 +71,6 @@ func DefaultConfiguration() Configuration {
} }
} }
// NetworkMap is a mapping from a network to a attributes.
type NetworkMap map[string]NetworkAttributes
// NetworkAttributes is a set of attributes attached to a network // NetworkAttributes is a set of attributes attached to a network
type NetworkAttributes struct { type NetworkAttributes struct {
// Name is a name attached to the network. May be unique or not. // Name is a name attached to the network. May be unique or not.
@@ -90,56 +85,28 @@ type NetworkAttributes struct {
Tenant string Tenant string
} }
// NetworkMapUnmarshallerHook decodes NetworkMap mapping and notably // NetworkAttributesUnmarshallerHook decodes network attributes. It
// check that valid networks are provided as key. It also accepts a // also accepts a string instead of attributes for backward
// string instead of attributes for backward compatibility. // compatibility.
func NetworkMapUnmarshallerHook() mapstructure.DecodeHookFunc { func NetworkAttributesUnmarshallerHook() mapstructure.DecodeHookFunc {
return func(from, to reflect.Type, data interface{}) (interface{}, error) { return func(from, to reflect.Value) (interface{}, error) {
if from.Kind() != reflect.Map || to != reflect.TypeOf(NetworkMap{}) { if to.Kind() == reflect.Interface {
return data, nil to = to.Elem()
} }
output := map[string]interface{}{} if from.Kind() == reflect.Interface {
iter := reflect.ValueOf(data).MapRange() from = from.Elem()
for i := 0; iter.Next(); i++ {
k := iter.Key()
v := iter.Value()
if k.Kind() == reflect.Interface {
k = k.Elem()
}
if k.Kind() != reflect.String {
return nil, fmt.Errorf("key %d is not a string (%s)", i, k.Kind())
}
// Parse key
_, ipNet, err := net.ParseCIDR(k.String())
if err != nil {
return nil, err
}
// Convert key to IPv6
ones, bits := ipNet.Mask.Size()
if bits != 32 && bits != 128 {
return nil, fmt.Errorf("key %d has an invalid netmask", i)
}
var key string
if bits == 32 {
key = fmt.Sprintf("::ffff:%s/%d", ipNet.IP.String(), ones+96)
} else {
key = ipNet.String()
}
// Parse value (partially)
if v.Kind() == reflect.Interface {
v = v.Elem()
}
if v.Kind() == reflect.String {
output[key] = NetworkAttributes{Name: v.String()}
} else {
output[key] = v.Interface()
}
} }
return output, nil if to.Type() != reflect.TypeOf(NetworkAttributes{}) {
return from.Interface(), nil
}
if from.Kind() == reflect.String {
return NetworkAttributes{Name: from.String()}, nil
}
return from.Interface(), nil
} }
} }
func init() { func init() {
helpers.AddMapstructureUnmarshallerHook(NetworkMapUnmarshallerHook()) helpers.AddMapstructureUnmarshallerHook(helpers.SubnetMapUnmarshallerHook[NetworkAttributes]())
helpers.AddMapstructureUnmarshallerHook(NetworkAttributesUnmarshallerHook())
} }

View File

@@ -16,33 +16,39 @@ func TestNetworkNamesUnmarshalHook(t *testing.T) {
cases := []struct { cases := []struct {
Description string Description string
Input map[string]interface{} Input map[string]interface{}
Output NetworkMap Output map[string]NetworkAttributes
Error bool Error bool
}{ }{
{ {
Description: "nil", Description: "nil",
Input: nil, Input: nil,
Output: NetworkMap{},
}, { }, {
Description: "empty", Description: "empty",
Input: gin.H{}, Input: gin.H{},
Output: NetworkMap{},
}, { }, {
Description: "IPv4", Description: "IPv4",
Input: gin.H{"203.0.113.0/24": gin.H{"name": "customer"}}, Input: gin.H{"203.0.113.0/24": gin.H{"name": "customer"}},
Output: NetworkMap{"::ffff:203.0.113.0/120": NetworkAttributes{Name: "customer"}}, Output: map[string]NetworkAttributes{
"203.0.113.0/24": {Name: "customer"},
},
}, { }, {
Description: "IPv6", Description: "IPv6",
Input: gin.H{"2001:db8:1::/64": gin.H{"name": "customer"}}, Input: gin.H{"2001:db8:1::/64": gin.H{"name": "customer"}},
Output: NetworkMap{"2001:db8:1::/64": NetworkAttributes{Name: "customer"}}, Output: map[string]NetworkAttributes{
"2001:db8:1::/64": {Name: "customer"},
},
}, { }, {
Description: "IPv4 subnet (compatibility)", Description: "IPv4 subnet (compatibility)",
Input: gin.H{"203.0.113.0/24": "customer"}, Input: gin.H{"203.0.113.0/24": "customer"},
Output: NetworkMap{"::ffff:203.0.113.0/120": NetworkAttributes{Name: "customer"}}, Output: map[string]NetworkAttributes{
"203.0.113.0/24": {Name: "customer"},
},
}, { }, {
Description: "IPv6 subnet (compatibility)", Description: "IPv6 subnet (compatibility)",
Input: gin.H{"2001:db8:1::/64": "customer"}, Input: gin.H{"2001:db8:1::/64": "customer"},
Output: NetworkMap{"2001:db8:1::/64": NetworkAttributes{Name: "customer"}}, Output: map[string]NetworkAttributes{
"2001:db8:1::/64": {Name: "customer"},
},
}, { }, {
Description: "all attributes", Description: "all attributes",
Input: gin.H{"203.0.113.0/24": gin.H{ Input: gin.H{"203.0.113.0/24": gin.H{
@@ -52,7 +58,7 @@ func TestNetworkNamesUnmarshalHook(t *testing.T) {
"region": "france", "region": "france",
"tenant": "mobile", "tenant": "mobile",
}}, }},
Output: NetworkMap{"::ffff:203.0.113.0/120": NetworkAttributes{ Output: map[string]NetworkAttributes{"203.0.113.0/24": {
Name: "customer1", Name: "customer1",
Role: "customer", Role: "customer",
Site: "paris", Site: "paris",
@@ -71,13 +77,8 @@ func TestNetworkNamesUnmarshalHook(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.Description, func(t *testing.T) { t.Run(tc.Description, func(t *testing.T) {
var got NetworkMap var got helpers.SubnetMap[NetworkAttributes]
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ decoder, err := mapstructure.NewDecoder(helpers.GetMapStructureDecoderConfig(&got))
Result: &got,
ErrorUnused: true,
Metadata: nil,
DecodeHook: NetworkMapUnmarshallerHook(),
})
if err != nil { if err != nil {
t.Fatalf("NewDecoder() error:\n%+v", err) t.Fatalf("NewDecoder() error:\n%+v", err)
} }
@@ -86,7 +87,7 @@ func TestNetworkNamesUnmarshalHook(t *testing.T) {
t.Fatalf("Decode() error:\n%+v", err) t.Fatalf("Decode() error:\n%+v", err)
} else if err == nil && tc.Error { } else if err == nil && tc.Error {
t.Fatal("Decode() did not return an error") t.Fatal("Decode() did not return an error")
} else if diff := helpers.Diff(got, tc.Output); diff != "" { } else if diff := helpers.Diff(got.ToMap(), tc.Output); diff != "" {
t.Fatalf("Decode() (-got, +want):\n%s", diff) t.Fatalf("Decode() (-got, +want):\n%s", diff)
} }
}) })

View File

@@ -60,7 +60,7 @@ func (c *Component) registerHTTPHandlers() error {
wr := csv.NewWriter(w) wr := csv.NewWriter(w)
wr.Write([]string{"network", "name", "role", "site", "region", "tenant"}) wr.Write([]string{"network", "name", "role", "site", "region", "tenant"})
if c.config.Networks != nil { if c.config.Networks != nil {
for k, v := range c.config.Networks { for k, v := range c.config.Networks.ToMap() {
wr.Write([]string{k, v.Name, v.Role, v.Site, v.Region, v.Tenant}) wr.Write([]string{k, v.Name, v.Role, v.Site, v.Region, v.Tenant})
} }
} }

View File

@@ -15,9 +15,9 @@ import (
func TestHTTPEndpoints(t *testing.T) { func TestHTTPEndpoints(t *testing.T) {
r := reporter.NewMock(t) r := reporter.NewMock(t)
config := DefaultConfiguration() config := DefaultConfiguration()
config.Networks = NetworkMap{ config.Networks = helpers.MustNewSubnetMap(map[string]NetworkAttributes{
"::ffff:192.0.2.0/24": NetworkAttributes{Name: "infra"}, "::ffff:192.0.2.0/120": {Name: "infra"},
} })
c, err := New(r, config, Dependencies{ c, err := New(r, config, Dependencies{
Daemon: daemon.NewMock(t), Daemon: daemon.NewMock(t),
HTTP: http.NewMock(t, r), HTTP: http.NewMock(t, r),
@@ -47,7 +47,7 @@ func TestHTTPEndpoints(t *testing.T) {
ContentType: "text/csv; charset=utf-8", ContentType: "text/csv; charset=utf-8",
FirstLines: []string{ FirstLines: []string{
`network,name,role,site,region,tenant`, `network,name,role,site,region,tenant`,
`::ffff:192.0.2.0/24,infra,,,,`, `192.0.2.0/24,infra,,,,`,
}, },
}, { }, {
URL: "/api/v0/orchestrator/clickhouse/init.sh", URL: "/api/v0/orchestrator/clickhouse/init.sh",