diff --git a/nomad/structs/node_class.go b/nomad/structs/node_class.go index 7cae65ef7..74b6d04e6 100644 --- a/nomad/structs/node_class.go +++ b/nomad/structs/node_class.go @@ -4,13 +4,14 @@ import ( "fmt" "strings" + "github.com/gobwas/glob" "github.com/mitchellh/hashstructure" ) const ( - // A suffix that can be appended to node meta keys to mark them for - // exclusion in computed node class. - NodeMetaUnique = "_unique" + // NodeUniqueSuffix is a suffix that can be appended to node meta or + // attribute keys to mark them for exclusion in computed node class. + NodeUniqueSuffix = "_unique" ) // ComputeClass computes a derived class for the node based on its attributes. @@ -18,7 +19,6 @@ const ( // attributes and capabilities. Thus, when calculating a node's computed class // we avoid including any uniquely identifing fields. func (n *Node) ComputeClass() error { - // TODO: Bucket node resources such as DiskMB/IOPS/etc. hash, err := hashstructure.Hash(n, nil) if err != nil { return err @@ -40,13 +40,45 @@ func (n Node) HashInclude(field string, v interface{}) (bool, error) { return false, nil case "CreateIndex", "ModifyIndex": // Raft indexes return false, nil - case "Reserved": // Doesn't effect placement capability + case "Resources", "Reserved": // Doesn't effect placement capability return false, nil default: return true, nil } } +var ( + // GlobalUniqueAttrs is a set of attributes that uniquely identify all + // nodes. It is stored once by the server, rather than by each node to + // reduce storage costs. + GlobalUniqueAttrs = []glob.Glob{ + glob.MustCompile("consul.name"), + glob.MustCompile("platform.gce.hostname"), + glob.MustCompile("platform.gce.id"), + glob.MustCompile("platform.gce.network.*.ip"), + glob.MustCompile("platform.gce.network.*.external-ip"), + glob.MustCompile("platform.aws.ami-id"), + glob.MustCompile("platform.aws.hostname"), + glob.MustCompile("platform.aws.instance-id"), + glob.MustCompile("platform.aws.local*"), + glob.MustCompile("platform.aws.public*"), + glob.MustCompile("network.ip-address"), + glob.MustCompile("storage.*"), // Ignore all storage + } +) + +// excludeAttr returns whether the key should be excluded when calculating +// computed node class. +func excludeAttr(key string) bool { + for _, g := range GlobalUniqueAttrs { + if g.Match(key) { + return true + } + } + + return false +} + // HashIncludeMap is used to blacklist uniquely identifying node map keys from being // included in the computed node class. func (n Node) HashIncludeMap(field string, k, v interface{}) (bool, error) { @@ -55,28 +87,15 @@ func (n Node) HashIncludeMap(field string, k, v interface{}) (bool, error) { return false, fmt.Errorf("map key %v not a string") } + // Check if the user marked the key as unique. + isUnique := strings.HasSuffix(key, NodeUniqueSuffix) + switch field { case "Attributes": - // Check if the key is marked as unique by the fingerprinters. - _, unique := n.UniqueAttributes[key] - return !unique, nil + return !excludeAttr(key) && !isUnique, nil case "Meta": - // Check if the user marked the key as unique. - return !strings.HasSuffix(key, NodeMetaUnique), nil + return !isUnique, nil default: return false, fmt.Errorf("unexpected map field: %v", field) } } - -// HashInclude is used to blacklist uniquely identifying network fields from being -// included in the computed node class. -func (n NetworkResource) HashInclude(field string, v interface{}) (bool, error) { - switch field { - case "IP", "CIDR": // Uniquely identifying - return false, nil - case "ReservedPorts", "DynamicPorts": // Doesn't effect placement capability - return false, nil - default: - return true, nil - } -} diff --git a/nomad/structs/node_class_test.go b/nomad/structs/node_class_test.go index dae377d94..5891bfda7 100644 --- a/nomad/structs/node_class_test.go +++ b/nomad/structs/node_class_test.go @@ -1,6 +1,7 @@ package structs import ( + "fmt" "testing" ) @@ -15,7 +16,6 @@ func testNode() *Node { "version": "0.1.0", "driver.exec": "1", }, - UniqueAttributes: make(map[string]struct{}), Resources: &Resources{ CPU: 4000, MemoryMB: 8192, @@ -29,19 +29,6 @@ func testNode() *Node { }, }, }, - Reserved: &Resources{ - CPU: 100, - MemoryMB: 256, - DiskMB: 4 * 1024, - Networks: []*NetworkResource{ - &NetworkResource{ - Device: "eth0", - IP: "192.168.0.100", - ReservedPorts: []Port{{Label: "main", Value: 22}}, - MBits: 1, - }, - }, - }, Links: map[string]string{ "consul": "foobar.dc1", }, @@ -111,46 +98,6 @@ func TestNode_ComputedClass_Ignore(t *testing.T) { } } -func TestNode_ComputedClass_NetworkResources(t *testing.T) { - // Create a node with a few network resources and gets it computed class - nr1 := &NetworkResource{ - Device: "eth0", - CIDR: "192.168.0.100/32", - MBits: 1000, - } - nr2 := &NetworkResource{ - Device: "eth1", - CIDR: "192.168.0.100/32", - MBits: 500, - } - n := &Node{ - Resources: &Resources{ - Networks: []*NetworkResource{nr1, nr2}, - }, - } - if err := n.ComputeClass(); err != nil { - t.Fatalf("ComputeClass() failed: %v", err) - } - if n.ComputedClass == 0 { - t.Fatal("ComputeClass() didn't set computed class") - } - old := n.ComputedClass - - // Change the order of the network resources and compute the class again. - n.Resources.Networks = []*NetworkResource{nr2, nr1} - if err := n.ComputeClass(); err != nil { - t.Fatalf("ComputeClass() failed: %v", err) - } - if n.ComputedClass == 0 { - t.Fatal("ComputeClass() didn't set computed class") - } - - if old != n.ComputedClass { - t.Fatal("ComputeClass() didn't ignore NetworkResource order") - } - -} - func TestNode_ComputedClass_Attr(t *testing.T) { // Create a node and gets it computed class n := testNode() @@ -162,6 +109,15 @@ func TestNode_ComputedClass_Attr(t *testing.T) { } old := n.ComputedClass + // Add a unique addr and compute the class again + n.Attributes[fmt.Sprintf("%s%s", "foo", NodeUniqueSuffix)] = "bar" + if err := n.ComputeClass(); err != nil { + t.Fatalf("ComputeClass() failed: %v", err) + } + if old != n.ComputedClass { + t.Fatal("ComputeClass() didn't ignore unique attr suffix") + } + // Modify an attribute and compute the class again. n.Attributes["version"] = "New Version" if err := n.ComputeClass(); err != nil { @@ -176,9 +132,7 @@ func TestNode_ComputedClass_Attr(t *testing.T) { old = n.ComputedClass // Add an ignored attribute and compute the class again. - key := "ignore" - n.Attributes[key] = "hello world" - n.UniqueAttributes[key] = struct{}{} + n.Attributes["network.ip-address"] = "hello world" if err := n.ComputeClass(); err != nil { t.Fatalf("ComputeClass() failed: %v", err) } diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index de6bfc096..ed82b7918 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -476,9 +476,6 @@ type Node struct { // "docker.runtime=1.8.3" Attributes map[string]string - // UniqueAttributes are attributes that uniquely identify a node. - UniqueAttributes map[string]struct{} - // Resources is the available resources on the client. // For example 'cpu=2' 'memory=2048' Resources *Resources @@ -570,7 +567,7 @@ type Resources struct { MemoryMB int `mapstructure:"memory"` DiskMB int `mapstructure:"disk"` IOPS int - Networks []*NetworkResource `hash:"set"` + Networks []*NetworkResource } // Copy returns a deep copy of the resources