diff --git a/client/client.go b/client/client.go index 757f3279900..705efb524e7 100644 --- a/client/client.go +++ b/client/client.go @@ -35,7 +35,6 @@ import ( "github.com/hashicorp/nomad/nomad/structs" nconfig "github.com/hashicorp/nomad/nomad/structs/config" vaultapi "github.com/hashicorp/vault/api" - "github.com/mitchellh/hashstructure" "github.com/shirou/gopsutil/host" ) @@ -130,6 +129,10 @@ type Client struct { // triggerDiscoveryCh triggers Consul discovery; see triggerDiscovery triggerDiscoveryCh chan struct{} + // triggerNodeUpdate triggers the client to mark the Node as changed and + // update it. + triggerNodeUpdate chan struct{} + // discovered will be ticked whenever Consul discovery completes // successfully serversDiscoveredCh chan struct{} @@ -209,6 +212,7 @@ func NewClient(cfg *config.Config, consulCatalog consul.CatalogAPI, consulServic allocUpdates: make(chan *structs.Allocation, 64), shutdownCh: make(chan struct{}), triggerDiscoveryCh: make(chan struct{}), + triggerNodeUpdate: make(chan struct{}, 8), serversDiscoveredCh: make(chan struct{}), } @@ -956,30 +960,81 @@ func (c *Client) reservePorts() { func (c *Client) updateNodeFromFingerprint(response *cstructs.FingerprintResponse) *structs.Node { c.configLock.Lock() defer c.configLock.Unlock() - for name, val := range response.Attributes { - if val == "" { + + nodeHasChanged := false + + for name, newVal := range response.Attributes { + oldVal := c.config.Node.Attributes[name] + if oldVal == newVal { + continue + } + + nodeHasChanged = true + if newVal == "" { delete(c.config.Node.Attributes, name) } else { - c.config.Node.Attributes[name] = val + c.config.Node.Attributes[name] = newVal } } // update node links and resources from the diff created from // fingerprinting - for name, val := range response.Links { - if val == "" { + for name, newVal := range response.Links { + oldVal := c.config.Node.Links[name] + if oldVal == newVal { + continue + } + + nodeHasChanged = true + if newVal == "" { delete(c.config.Node.Links, name) } else { - c.config.Node.Links[name] = val + c.config.Node.Links[name] = newVal } } - if response.Resources != nil { + if response.Resources != nil && !resourcesAreEqual(c.config.Node.Resources, response.Resources) { + nodeHasChanged = true c.config.Node.Resources.Merge(response.Resources) } + + if nodeHasChanged { + c.updateNode() + } return c.config.Node } +// resourcesAreEqual is a temporary function to compare whether resources are +// equal. We can use this until we change fingerprinters to set pointers on a +// return type. +func resourcesAreEqual(first, second *structs.Resources) bool { + if first.CPU != second.CPU { + return false + } + if first.MemoryMB != second.MemoryMB { + return false + } + if first.DiskMB != second.DiskMB { + return false + } + if first.IOPS != second.IOPS { + return false + } + if len(first.Networks) != len(second.Networks) { + return false + } + for i, e := range first.Networks { + if len(second.Networks) < i { + return false + } + f := second.Networks[i] + if !e.Equals(f) { + return false + } + } + return true +} + // retryIntv calculates a retry interval value given the base func (c *Client) retryIntv(base time.Duration) time.Duration { if c.config.DevMode { @@ -991,21 +1046,11 @@ func (c *Client) retryIntv(base time.Duration) time.Duration { // registerAndHeartbeat is a long lived goroutine used to register the client // and then start heartbeatng to the server. func (c *Client) registerAndHeartbeat() { - // Before registering capture the hashes of the Node's attribute and - // metadata maps. The hashes may be out of date with what registers but this - // is okay since the loop checking for node updates will detect this and - // reregister. This is necessary to avoid races between the periodic - // fingerprinters and the node registering. - attrHash, metaHash, err := nodeMapHashes(c.Node()) - if err != nil { - c.logger.Printf("[ERR] client: failed to determine initial node hashes. May result in stale node being registered: %v", err) - } - // Register the node c.retryRegisterNode() // Start watching changes for node changes - go c.watchNodeUpdates(attrHash, metaHash) + go c.watchNodeUpdates() // Setup the heartbeat timer, for the initial registration // we want to do this quickly. We want to do it extra quickly @@ -1086,40 +1131,6 @@ func (c *Client) run() { } } -// nodeMapHashes returns the hashes of the passed Node's attribute and metadata -// maps. -func nodeMapHashes(node *structs.Node) (attrHash, metaHash uint64, err error) { - attrHash, err = hashstructure.Hash(node.Attributes, nil) - if err != nil { - return 0, 0, fmt.Errorf("unable to calculate node attributes hash: %v", err) - } - // Calculate node meta map hash - metaHash, err = hashstructure.Hash(node.Meta, nil) - if err != nil { - return 0, 0, fmt.Errorf("unable to calculate node meta hash: %v", err) - } - return attrHash, metaHash, nil -} - -// hasNodeChanged calculates a hash for the node attributes- and meta map. -// The new hash values are compared against the old (passed-in) hash values to -// determine if the node properties have changed. It returns the new hash values -// in case they are different from the old hash values. -func (c *Client) hasNodeChanged(oldAttrHash uint64, oldMetaHash uint64) (bool, uint64, uint64) { - c.configLock.RLock() - defer c.configLock.RUnlock() - - // Check if the Node that is being updated by fingerprinters has changed. - newAttrHash, newMetaHash, err := nodeMapHashes(c.config.Node) - if err != nil { - c.logger.Printf("[DEBUG] client: unable to calculate node hashes: %v", err) - } - if newAttrHash != oldAttrHash || newMetaHash != oldMetaHash { - return true, newAttrHash, newMetaHash - } - return false, oldAttrHash, oldMetaHash -} - // retryRegisterNode is used to register the node or update the registration and // retry in case of failure. func (c *Client) retryRegisterNode() { @@ -1512,28 +1523,44 @@ OUTER: } } -// watchNodeUpdates periodically checks for changes to the node attributes or -// meta map. The passed hashes are the initial hash values for the attribute and -// metadata of the node respectively. -func (c *Client) watchNodeUpdates(attrHash, metaHash uint64) { - c.logger.Printf("[DEBUG] client: periodically checking for node changes at duration %v", nodeUpdateRetryIntv) +// updateNode triggers a client to update its node copy if it isn't doing +// so already +func (c *Client) updateNode() { + select { + case c.triggerNodeUpdate <- struct{}{}: + // Node update goroutine was released to execute + default: + // Node update goroutine was already running + } +} + +// watchNodeUpdates blocks until it is edge triggered. Once triggered, +// it will update the client node copy and re-register the node. +func (c *Client) watchNodeUpdates() { + var hasChanged bool + timer := time.NewTimer(c.retryIntv(nodeUpdateRetryIntv)) + defer timer.Stop() - var changed bool for { select { - case <-time.After(c.retryIntv(nodeUpdateRetryIntv)): - changed, attrHash, metaHash = c.hasNodeChanged(attrHash, metaHash) - if changed { - c.logger.Printf("[DEBUG] client: state changed, updating node.") + case <-timer.C: + c.logger.Printf("[DEBUG] client: state changed, updating node and re-registering.") - // Update the config copy. - c.configLock.Lock() - node := c.config.Node.Copy() - c.configCopy.Node = node - c.configLock.Unlock() + // Update the config copy. + c.configLock.Lock() + node := c.config.Node.Copy() + c.configCopy.Node = node + c.configLock.Unlock() - c.retryRegisterNode() + c.retryRegisterNode() + + hasChanged = false + case <-c.triggerNodeUpdate: + if hasChanged { + continue } + hasChanged = true + timer.Reset(c.retryIntv(nodeUpdateRetryIntv)) case <-c.shutdownCh: return } diff --git a/client/client_test.go b/client/client_test.go index 13d4debfb01..de5d35ee997 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -19,7 +19,6 @@ import ( "github.com/hashicorp/nomad/nomad/structs" nconfig "github.com/hashicorp/nomad/nomad/structs/config" "github.com/hashicorp/nomad/testutil" - "github.com/mitchellh/hashstructure" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -126,38 +125,6 @@ func TestClient_Fingerprint(t *testing.T) { require.NotEqual("", node.Attributes["driver.mock_driver"]) } -func TestClient_HasNodeChanged(t *testing.T) { - t.Parallel() - c := TestClient(t, nil) - defer c.Shutdown() - - node := c.config.Node - attrHash, err := hashstructure.Hash(node.Attributes, nil) - if err != nil { - c.logger.Printf("[DEBUG] client: unable to calculate node attributes hash: %v", err) - } - // Calculate node meta map hash - metaHash, err := hashstructure.Hash(node.Meta, nil) - if err != nil { - c.logger.Printf("[DEBUG] client: unable to calculate node meta hash: %v", err) - } - if changed, _, _ := c.hasNodeChanged(attrHash, metaHash); changed { - t.Fatalf("Unexpected hash change.") - } - - // Change node attribute - node.Attributes["arch"] = "xyz_86" - if changed, newAttrHash, _ := c.hasNodeChanged(attrHash, metaHash); !changed { - t.Fatalf("Expected hash change in attributes: %d vs %d", attrHash, newAttrHash) - } - - // Change node meta map - node.Meta["foo"] = "bar" - if changed, _, newMetaHash := c.hasNodeChanged(attrHash, metaHash); !changed { - t.Fatalf("Expected hash change in meta map: %d vs %d", metaHash, newMetaHash) - } -} - func TestClient_Fingerprint_Periodic(t *testing.T) { driver.CheckForMockDriver(t) t.Parallel() @@ -166,8 +133,8 @@ func TestClient_Fingerprint_Periodic(t *testing.T) { // our linter without explicit disabling. c1 := TestClient(t, func(c *config.Config) { c.Options = map[string]string{ - driver.ShutdownPeriodicAfter: "true", // nolint: varcheck - driver.ShutdownPeriodicDuration: "3", // nolint: varcheck + driver.ShutdownPeriodicAfter: "true", + driver.ShutdownPeriodicDuration: "3", } }) defer c1.Shutdown() diff --git a/client/fingerprint_manager.go b/client/fingerprint_manager.go index a191555aaad..713189604c0 100644 --- a/client/fingerprint_manager.go +++ b/client/fingerprint_manager.go @@ -104,11 +104,11 @@ func (fm *FingerprintManager) fingerprint(name string, f fingerprint.Fingerprint return false, err } - fm.nodeLock.Lock() if node := fm.updateNode(&response); node != nil { + fm.nodeLock.Lock() fm.node = node + fm.nodeLock.Unlock() } - fm.nodeLock.Unlock() return response.Detected, nil } diff --git a/client/fingerprint_manager_test.go b/client/fingerprint_manager_test.go index fe37966e3e3..9c3baa1061b 100644 --- a/client/fingerprint_manager_test.go +++ b/client/fingerprint_manager_test.go @@ -6,7 +6,6 @@ import ( "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/driver" - cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" "github.com/stretchr/testify/require" @@ -19,14 +18,43 @@ func TestFingerprintManager_Run_MockDriver(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } + testConfig := config.Config{Node: node} + testClient := &Client{config: &testConfig} + conf := config.DefaultConfig() - updateNode := func(r *cstructs.FingerprintResponse) *structs.Node { - for k, v := range r.Attributes { - node.Attributes[k] = v - } - return node + getConfig := func() *config.Config { + return conf } + + fm := NewFingerprintManager( + getConfig, + node, + make(chan struct{}), + testClient.updateNodeFromFingerprint, + testLogger(), + ) + + err := fm.Run() + require.Nil(err) + require.NotEqual("", node.Attributes["driver.mock_driver"]) +} + +func TestFingerprintManager_Run_ResourcesFingerprint(t *testing.T) { + driver.CheckForMockDriver(t) + t.Parallel() + require := require.New(t) + + node := &structs.Node{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + testConfig := config.Config{Node: node} + testClient := &Client{config: &testConfig} + conf := config.DefaultConfig() getConfig := func() *config.Config { return conf @@ -36,13 +64,15 @@ func TestFingerprintManager_Run_MockDriver(t *testing.T) { getConfig, node, make(chan struct{}), - updateNode, + testClient.updateNodeFromFingerprint, testLogger(), ) err := fm.Run() require.Nil(err) - require.NotEqual("", node.Attributes["driver.mock_driver"]) + require.NotEqual(0, node.Resources.CPU) + require.NotEqual(0, node.Resources.MemoryMB) + require.NotZero(node.Resources.DiskMB) } func TestFingerprintManager_Fingerprint_Run(t *testing.T) { @@ -51,7 +81,11 @@ func TestFingerprintManager_Fingerprint_Run(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } + testConfig := config.Config{Node: node} + testClient := &Client{config: &testConfig} conf := config.DefaultConfig() conf.Options = map[string]string{"driver.raw_exec.enable": "true"} @@ -59,18 +93,11 @@ func TestFingerprintManager_Fingerprint_Run(t *testing.T) { return conf } - updateNode := func(r *cstructs.FingerprintResponse) *structs.Node { - for k, v := range r.Attributes { - node.Attributes[k] = v - } - return node - } - fm := NewFingerprintManager( getConfig, node, make(chan struct{}), - updateNode, + testClient.updateNodeFromFingerprint, testLogger(), ) @@ -86,13 +113,12 @@ func TestFingerprintManager_Fingerprint_Periodic(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } - updateNode := func(r *cstructs.FingerprintResponse) *structs.Node { - for k, v := range r.Attributes { - node.Attributes[k] = v - } - return node - } + testConfig := config.Config{Node: node} + testClient := &Client{config: &testConfig} + conf := config.DefaultConfig() conf.Options = map[string]string{ "test.shutdown_periodic_after": "true", @@ -111,7 +137,7 @@ func TestFingerprintManager_Fingerprint_Periodic(t *testing.T) { getConfig, node, shutdownCh, - updateNode, + testClient.updateNodeFromFingerprint, testLogger(), ) @@ -147,13 +173,12 @@ func TestFimgerprintManager_Run_InWhitelist(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } - updateNode := func(r *cstructs.FingerprintResponse) *structs.Node { - for k, v := range r.Attributes { - node.Attributes[k] = v - } - return node - } + testConfig := config.Config{Node: node} + testClient := &Client{config: &testConfig} + conf := config.DefaultConfig() conf.Options = map[string]string{"fingerprint.whitelist": " arch,cpu,memory,network,storage,foo,bar "} getConfig := func() *config.Config { @@ -169,7 +194,7 @@ func TestFimgerprintManager_Run_InWhitelist(t *testing.T) { getConfig, node, shutdownCh, - updateNode, + testClient.updateNodeFromFingerprint, testLogger(), ) @@ -184,13 +209,12 @@ func TestFimgerprintManager_Run_InBlacklist(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } - updateNode := func(r *cstructs.FingerprintResponse) *structs.Node { - for k, v := range r.Attributes { - node.Attributes[k] = v - } - return node - } + testConfig := config.Config{Node: node} + testClient := &Client{config: &testConfig} + conf := config.DefaultConfig() conf.Options = map[string]string{"fingerprint.whitelist": " arch,memory,foo,bar "} conf.Options = map[string]string{"fingerprint.blacklist": " cpu "} @@ -207,7 +231,7 @@ func TestFimgerprintManager_Run_InBlacklist(t *testing.T) { getConfig, node, shutdownCh, - updateNode, + testClient.updateNodeFromFingerprint, testLogger(), ) @@ -223,13 +247,12 @@ func TestFimgerprintManager_Run_Combination(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } - updateNode := func(r *cstructs.FingerprintResponse) *structs.Node { - for k, v := range r.Attributes { - node.Attributes[k] = v - } - return node - } + testConfig := config.Config{Node: node} + testClient := &Client{config: &testConfig} + conf := config.DefaultConfig() conf.Options = map[string]string{"fingerprint.whitelist": " arch,cpu,memory,foo,bar "} conf.Options = map[string]string{"fingerprint.blacklist": " memory,nomad "} @@ -246,7 +269,7 @@ func TestFimgerprintManager_Run_Combination(t *testing.T) { getConfig, node, shutdownCh, - updateNode, + testClient.updateNodeFromFingerprint, testLogger(), ) @@ -264,13 +287,12 @@ func TestFimgerprintManager_Run_WhitelistDrivers(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } - updateNode := func(r *cstructs.FingerprintResponse) *structs.Node { - for k, v := range r.Attributes { - node.Attributes[k] = v - } - return node - } + testConfig := config.Config{Node: node} + testClient := &Client{config: &testConfig} + conf := config.DefaultConfig() conf.Options = map[string]string{ "driver.raw_exec.enable": "1", @@ -289,7 +311,7 @@ func TestFimgerprintManager_Run_WhitelistDrivers(t *testing.T) { getConfig, node, shutdownCh, - updateNode, + testClient.updateNodeFromFingerprint, testLogger(), ) @@ -304,13 +326,12 @@ func TestFimgerprintManager_Run_AllDriversBlacklisted(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } - updateNode := func(r *cstructs.FingerprintResponse) *structs.Node { - for k, v := range r.Attributes { - node.Attributes[k] = v - } - return node - } + testConfig := config.Config{Node: node} + testClient := &Client{config: &testConfig} + conf := config.DefaultConfig() conf.Options = map[string]string{ "driver.whitelist": " foo,bar,baz ", @@ -328,7 +349,7 @@ func TestFimgerprintManager_Run_AllDriversBlacklisted(t *testing.T) { getConfig, node, shutdownCh, - updateNode, + testClient.updateNodeFromFingerprint, testLogger(), ) @@ -345,13 +366,12 @@ func TestFimgerprintManager_Run_DriversWhiteListBlacklistCombination(t *testing. node := &structs.Node{ Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } - updateNode := func(r *cstructs.FingerprintResponse) *structs.Node { - for k, v := range r.Attributes { - node.Attributes[k] = v - } - return node - } + testConfig := config.Config{Node: node} + testClient := &Client{config: &testConfig} + conf := config.DefaultConfig() conf.Options = map[string]string{ "driver.raw_exec.enable": "1", @@ -371,7 +391,7 @@ func TestFimgerprintManager_Run_DriversWhiteListBlacklistCombination(t *testing. getConfig, node, shutdownCh, - updateNode, + testClient.updateNodeFromFingerprint, testLogger(), ) @@ -390,12 +410,8 @@ func TestFimgerprintManager_Run_DriversInBlacklist(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string, 0), - } - updateNode := func(r *cstructs.FingerprintResponse) *structs.Node { - for k, v := range r.Attributes { - node.Attributes[k] = v - } - return node + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } conf := config.DefaultConfig() conf.Options = map[string]string{ @@ -403,9 +419,9 @@ func TestFimgerprintManager_Run_DriversInBlacklist(t *testing.T) { "driver.whitelist": " raw_exec,foo,bar,baz ", "driver.blacklist": " exec,foo,bar,baz ", } - getConfig := func() *config.Config { - return conf - } + conf.Node = node + + testClient := &Client{config: conf} shutdownCh := make(chan struct{}) defer (func() { @@ -413,10 +429,10 @@ func TestFimgerprintManager_Run_DriversInBlacklist(t *testing.T) { })() fm := NewFingerprintManager( - getConfig, + testClient.GetConfig, node, shutdownCh, - updateNode, + testClient.updateNodeFromFingerprint, testLogger(), ) diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 52630853383..7f3e2e3c3a4 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -1432,6 +1432,50 @@ type NetworkResource struct { DynamicPorts []Port // Host Dynamically assigned ports } +func (nr *NetworkResource) Equals(other *NetworkResource) bool { + if nr.Device != other.Device { + return false + } + + if nr.CIDR != other.CIDR { + return false + } + + if nr.IP != other.IP { + return false + } + + if nr.MBits != other.MBits { + return false + } + + if len(nr.ReservedPorts) != len(other.ReservedPorts) { + return false + } + + for i, port := range nr.ReservedPorts { + if len(other.ReservedPorts) <= i { + return false + } + if port != other.ReservedPorts[i] { + return false + } + } + + if len(nr.DynamicPorts) != len(other.DynamicPorts) { + return false + } + for i, port := range nr.DynamicPorts { + if len(other.DynamicPorts) <= i { + return false + } + if port != other.DynamicPorts[i] { + return false + } + } + return true +} + func (n *NetworkResource) Canonicalize() { // Ensure that an empty and nil slices are treated the same to avoid scheduling // problems since we use reflect DeepEquals. diff --git a/nomad/structs/structs_test.go b/nomad/structs/structs_test.go index e7d0fb0f5b7..fa1b132d3b6 100644 --- a/nomad/structs/structs_test.go +++ b/nomad/structs/structs_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/nomad/helper/uuid" "github.com/kr/pretty" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestJob_Validate(t *testing.T) { @@ -3104,3 +3105,162 @@ func TestTaskEventPopulate(t *testing.T) { } } } + +func TestNetworkResourcesEquals(t *testing.T) { + require := require.New(t) + var networkResourcesTest = []struct { + input []*NetworkResource + expected bool + errorMsg string + }{ + { + []*NetworkResource{ + { + IP: "10.0.0.1", + MBits: 50, + ReservedPorts: []Port{{"web", 80}}, + }, + { + IP: "10.0.0.1", + MBits: 50, + ReservedPorts: []Port{{"web", 80}}, + }, + }, + true, + "Equal network resources should return true", + }, + { + []*NetworkResource{ + { + IP: "10.0.0.0", + MBits: 50, + ReservedPorts: []Port{{"web", 80}}, + }, + { + IP: "10.0.0.1", + MBits: 50, + ReservedPorts: []Port{{"web", 80}}, + }, + }, + false, + "Different IP addresses should return false", + }, + { + []*NetworkResource{ + { + IP: "10.0.0.1", + MBits: 40, + ReservedPorts: []Port{{"web", 80}}, + }, + { + IP: "10.0.0.1", + MBits: 50, + ReservedPorts: []Port{{"web", 80}}, + }, + }, + false, + "Different MBits values should return false", + }, + { + []*NetworkResource{ + { + IP: "10.0.0.1", + MBits: 50, + ReservedPorts: []Port{{"web", 80}}, + }, + { + IP: "10.0.0.1", + MBits: 50, + ReservedPorts: []Port{{"web", 80}, {"web", 80}}, + }, + }, + false, + "Different ReservedPorts lengths should return false", + }, + { + []*NetworkResource{ + { + IP: "10.0.0.1", + MBits: 50, + ReservedPorts: []Port{{"web", 80}}, + }, + { + IP: "10.0.0.1", + MBits: 50, + ReservedPorts: []Port{}, + }, + }, + false, + "Empty and non empty ReservedPorts values should return false", + }, + { + []*NetworkResource{ + { + IP: "10.0.0.1", + MBits: 50, + ReservedPorts: []Port{{"web", 80}}, + }, + { + IP: "10.0.0.1", + MBits: 50, + ReservedPorts: []Port{{"notweb", 80}}, + }, + }, + false, + "Different valued ReservedPorts values should return false", + }, + { + []*NetworkResource{ + { + IP: "10.0.0.1", + MBits: 50, + DynamicPorts: []Port{{"web", 80}}, + }, + { + IP: "10.0.0.1", + MBits: 50, + DynamicPorts: []Port{{"web", 80}, {"web", 80}}, + }, + }, + false, + "Different DynamicPorts lengths should return false", + }, + { + []*NetworkResource{ + { + IP: "10.0.0.1", + MBits: 50, + DynamicPorts: []Port{{"web", 80}}, + }, + { + IP: "10.0.0.1", + MBits: 50, + DynamicPorts: []Port{}, + }, + }, + false, + "Empty and non empty DynamicPorts values should return false", + }, + { + []*NetworkResource{ + { + IP: "10.0.0.1", + MBits: 50, + DynamicPorts: []Port{{"web", 80}}, + }, + { + IP: "10.0.0.1", + MBits: 50, + DynamicPorts: []Port{{"notweb", 80}}, + }, + }, + false, + "Different valued DynamicPorts values should return false", + }, + } + for _, testCase := range networkResourcesTest { + first := testCase.input[0] + second := testCase.input[1] + require.Equal(testCase.expected, first.Equals(second), testCase.errorMsg) + } +}