github.com/ncodes/nomad@v0.5.7-0.20170403112158-97adf4a74fb3/scheduler/feasible_test.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"testing"
     7  
     8  	"github.com/ncodes/nomad/nomad/mock"
     9  	"github.com/ncodes/nomad/nomad/structs"
    10  )
    11  
    12  func TestStaticIterator_Reset(t *testing.T) {
    13  	_, ctx := testContext(t)
    14  	var nodes []*structs.Node
    15  	for i := 0; i < 3; i++ {
    16  		nodes = append(nodes, mock.Node())
    17  	}
    18  	static := NewStaticIterator(ctx, nodes)
    19  
    20  	for i := 0; i < 6; i++ {
    21  		static.Reset()
    22  		for j := 0; j < i; j++ {
    23  			static.Next()
    24  		}
    25  		static.Reset()
    26  
    27  		out := collectFeasible(static)
    28  		if len(out) != len(nodes) {
    29  			t.Fatalf("out: %#v", out)
    30  			t.Fatalf("missing nodes %d %#v", i, static)
    31  		}
    32  
    33  		ids := make(map[string]struct{})
    34  		for _, o := range out {
    35  			if _, ok := ids[o.ID]; ok {
    36  				t.Fatalf("duplicate")
    37  			}
    38  			ids[o.ID] = struct{}{}
    39  		}
    40  	}
    41  }
    42  
    43  func TestStaticIterator_SetNodes(t *testing.T) {
    44  	_, ctx := testContext(t)
    45  	var nodes []*structs.Node
    46  	for i := 0; i < 3; i++ {
    47  		nodes = append(nodes, mock.Node())
    48  	}
    49  	static := NewStaticIterator(ctx, nodes)
    50  
    51  	newNodes := []*structs.Node{mock.Node()}
    52  	static.SetNodes(newNodes)
    53  
    54  	out := collectFeasible(static)
    55  	if !reflect.DeepEqual(out, newNodes) {
    56  		t.Fatalf("bad: %#v", out)
    57  	}
    58  }
    59  
    60  func TestRandomIterator(t *testing.T) {
    61  	_, ctx := testContext(t)
    62  	var nodes []*structs.Node
    63  	for i := 0; i < 10; i++ {
    64  		nodes = append(nodes, mock.Node())
    65  	}
    66  
    67  	nc := make([]*structs.Node, len(nodes))
    68  	copy(nc, nodes)
    69  	rand := NewRandomIterator(ctx, nc)
    70  
    71  	out := collectFeasible(rand)
    72  	if len(out) != len(nodes) {
    73  		t.Fatalf("missing nodes")
    74  	}
    75  	if reflect.DeepEqual(out, nodes) {
    76  		t.Fatalf("same order")
    77  	}
    78  }
    79  
    80  func TestDriverChecker(t *testing.T) {
    81  	_, ctx := testContext(t)
    82  	nodes := []*structs.Node{
    83  		mock.Node(),
    84  		mock.Node(),
    85  		mock.Node(),
    86  		mock.Node(),
    87  	}
    88  	nodes[0].Attributes["driver.foo"] = "1"
    89  	nodes[1].Attributes["driver.foo"] = "0"
    90  	nodes[2].Attributes["driver.foo"] = "true"
    91  	nodes[3].Attributes["driver.foo"] = "False"
    92  
    93  	drivers := map[string]struct{}{
    94  		"exec": struct{}{},
    95  		"foo":  struct{}{},
    96  	}
    97  	checker := NewDriverChecker(ctx, drivers)
    98  	cases := []struct {
    99  		Node   *structs.Node
   100  		Result bool
   101  	}{
   102  		{
   103  			Node:   nodes[0],
   104  			Result: true,
   105  		},
   106  		{
   107  			Node:   nodes[1],
   108  			Result: false,
   109  		},
   110  		{
   111  			Node:   nodes[2],
   112  			Result: true,
   113  		},
   114  		{
   115  			Node:   nodes[3],
   116  			Result: false,
   117  		},
   118  	}
   119  
   120  	for i, c := range cases {
   121  		if act := checker.Feasible(c.Node); act != c.Result {
   122  			t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result)
   123  		}
   124  	}
   125  }
   126  
   127  func TestConstraintChecker(t *testing.T) {
   128  	_, ctx := testContext(t)
   129  	nodes := []*structs.Node{
   130  		mock.Node(),
   131  		mock.Node(),
   132  		mock.Node(),
   133  		mock.Node(),
   134  	}
   135  
   136  	nodes[0].Attributes["kernel.name"] = "freebsd"
   137  	nodes[1].Datacenter = "dc2"
   138  	nodes[2].NodeClass = "large"
   139  
   140  	constraints := []*structs.Constraint{
   141  		&structs.Constraint{
   142  			Operand: "=",
   143  			LTarget: "${node.datacenter}",
   144  			RTarget: "dc1",
   145  		},
   146  		&structs.Constraint{
   147  			Operand: "is",
   148  			LTarget: "${attr.kernel.name}",
   149  			RTarget: "linux",
   150  		},
   151  		&structs.Constraint{
   152  			Operand: "is",
   153  			LTarget: "${node.class}",
   154  			RTarget: "large",
   155  		},
   156  	}
   157  	checker := NewConstraintChecker(ctx, constraints)
   158  	cases := []struct {
   159  		Node   *structs.Node
   160  		Result bool
   161  	}{
   162  		{
   163  			Node:   nodes[0],
   164  			Result: false,
   165  		},
   166  		{
   167  			Node:   nodes[1],
   168  			Result: false,
   169  		},
   170  		{
   171  			Node:   nodes[2],
   172  			Result: true,
   173  		},
   174  	}
   175  
   176  	for i, c := range cases {
   177  		if act := checker.Feasible(c.Node); act != c.Result {
   178  			t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result)
   179  		}
   180  	}
   181  }
   182  
   183  func TestResolveConstraintTarget(t *testing.T) {
   184  	type tcase struct {
   185  		target string
   186  		node   *structs.Node
   187  		val    interface{}
   188  		result bool
   189  	}
   190  	node := mock.Node()
   191  	cases := []tcase{
   192  		{
   193  			target: "${node.unique.id}",
   194  			node:   node,
   195  			val:    node.ID,
   196  			result: true,
   197  		},
   198  		{
   199  			target: "${node.datacenter}",
   200  			node:   node,
   201  			val:    node.Datacenter,
   202  			result: true,
   203  		},
   204  		{
   205  			target: "${node.unique.name}",
   206  			node:   node,
   207  			val:    node.Name,
   208  			result: true,
   209  		},
   210  		{
   211  			target: "${node.class}",
   212  			node:   node,
   213  			val:    node.NodeClass,
   214  			result: true,
   215  		},
   216  		{
   217  			target: "${node.foo}",
   218  			node:   node,
   219  			result: false,
   220  		},
   221  		{
   222  			target: "${attr.kernel.name}",
   223  			node:   node,
   224  			val:    node.Attributes["kernel.name"],
   225  			result: true,
   226  		},
   227  		{
   228  			target: "${attr.rand}",
   229  			node:   node,
   230  			result: false,
   231  		},
   232  		{
   233  			target: "${meta.pci-dss}",
   234  			node:   node,
   235  			val:    node.Meta["pci-dss"],
   236  			result: true,
   237  		},
   238  		{
   239  			target: "${meta.rand}",
   240  			node:   node,
   241  			result: false,
   242  		},
   243  	}
   244  
   245  	for _, tc := range cases {
   246  		res, ok := resolveConstraintTarget(tc.target, tc.node)
   247  		if ok != tc.result {
   248  			t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok)
   249  		}
   250  		if ok && !reflect.DeepEqual(res, tc.val) {
   251  			t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok)
   252  		}
   253  	}
   254  }
   255  
   256  func TestCheckConstraint(t *testing.T) {
   257  	type tcase struct {
   258  		op         string
   259  		lVal, rVal interface{}
   260  		result     bool
   261  	}
   262  	cases := []tcase{
   263  		{
   264  			op:   "=",
   265  			lVal: "foo", rVal: "foo",
   266  			result: true,
   267  		},
   268  		{
   269  			op:   "is",
   270  			lVal: "foo", rVal: "foo",
   271  			result: true,
   272  		},
   273  		{
   274  			op:   "==",
   275  			lVal: "foo", rVal: "foo",
   276  			result: true,
   277  		},
   278  		{
   279  			op:   "!=",
   280  			lVal: "foo", rVal: "foo",
   281  			result: false,
   282  		},
   283  		{
   284  			op:   "!=",
   285  			lVal: "foo", rVal: "bar",
   286  			result: true,
   287  		},
   288  		{
   289  			op:   "not",
   290  			lVal: "foo", rVal: "bar",
   291  			result: true,
   292  		},
   293  		{
   294  			op:   structs.ConstraintVersion,
   295  			lVal: "1.2.3", rVal: "~> 1.0",
   296  			result: true,
   297  		},
   298  		{
   299  			op:   structs.ConstraintRegex,
   300  			lVal: "foobarbaz", rVal: "[\\w]+",
   301  			result: true,
   302  		},
   303  		{
   304  			op:   "<",
   305  			lVal: "foo", rVal: "bar",
   306  			result: false,
   307  		},
   308  		{
   309  			op:   structs.ConstraintSetContains,
   310  			lVal: "foo,bar,baz", rVal: "foo,  bar  ",
   311  			result: true,
   312  		},
   313  		{
   314  			op:   structs.ConstraintSetContains,
   315  			lVal: "foo,bar,baz", rVal: "foo,bam",
   316  			result: false,
   317  		},
   318  	}
   319  
   320  	for _, tc := range cases {
   321  		_, ctx := testContext(t)
   322  		if res := checkConstraint(ctx, tc.op, tc.lVal, tc.rVal); res != tc.result {
   323  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   324  		}
   325  	}
   326  }
   327  
   328  func TestCheckLexicalOrder(t *testing.T) {
   329  	type tcase struct {
   330  		op         string
   331  		lVal, rVal interface{}
   332  		result     bool
   333  	}
   334  	cases := []tcase{
   335  		{
   336  			op:   "<",
   337  			lVal: "bar", rVal: "foo",
   338  			result: true,
   339  		},
   340  		{
   341  			op:   "<=",
   342  			lVal: "foo", rVal: "foo",
   343  			result: true,
   344  		},
   345  		{
   346  			op:   ">",
   347  			lVal: "bar", rVal: "foo",
   348  			result: false,
   349  		},
   350  		{
   351  			op:   ">=",
   352  			lVal: "bar", rVal: "bar",
   353  			result: true,
   354  		},
   355  		{
   356  			op:   ">",
   357  			lVal: 1, rVal: "foo",
   358  			result: false,
   359  		},
   360  	}
   361  	for _, tc := range cases {
   362  		if res := checkLexicalOrder(tc.op, tc.lVal, tc.rVal); res != tc.result {
   363  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   364  		}
   365  	}
   366  }
   367  
   368  func TestCheckVersionConstraint(t *testing.T) {
   369  	type tcase struct {
   370  		lVal, rVal interface{}
   371  		result     bool
   372  	}
   373  	cases := []tcase{
   374  		{
   375  			lVal: "1.2.3", rVal: "~> 1.0",
   376  			result: true,
   377  		},
   378  		{
   379  			lVal: "1.2.3", rVal: ">= 1.0, < 1.4",
   380  			result: true,
   381  		},
   382  		{
   383  			lVal: "2.0.1", rVal: "~> 1.0",
   384  			result: false,
   385  		},
   386  		{
   387  			lVal: "1.4", rVal: ">= 1.0, < 1.4",
   388  			result: false,
   389  		},
   390  		{
   391  			lVal: 1, rVal: "~> 1.0",
   392  			result: true,
   393  		},
   394  	}
   395  	for _, tc := range cases {
   396  		_, ctx := testContext(t)
   397  		if res := checkVersionConstraint(ctx, tc.lVal, tc.rVal); res != tc.result {
   398  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   399  		}
   400  	}
   401  }
   402  
   403  func TestCheckRegexpConstraint(t *testing.T) {
   404  	type tcase struct {
   405  		lVal, rVal interface{}
   406  		result     bool
   407  	}
   408  	cases := []tcase{
   409  		{
   410  			lVal: "foobar", rVal: "bar",
   411  			result: true,
   412  		},
   413  		{
   414  			lVal: "foobar", rVal: "^foo",
   415  			result: true,
   416  		},
   417  		{
   418  			lVal: "foobar", rVal: "^bar",
   419  			result: false,
   420  		},
   421  		{
   422  			lVal: "zipzap", rVal: "foo",
   423  			result: false,
   424  		},
   425  		{
   426  			lVal: 1, rVal: "foo",
   427  			result: false,
   428  		},
   429  	}
   430  	for _, tc := range cases {
   431  		_, ctx := testContext(t)
   432  		if res := checkRegexpConstraint(ctx, tc.lVal, tc.rVal); res != tc.result {
   433  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   434  		}
   435  	}
   436  }
   437  
   438  // This test puts allocations on the node to test if it detects infeasibility of
   439  // nodes correctly and picks the only feasible one
   440  func TestDistinctHostsIterator_JobDistinctHosts(t *testing.T) {
   441  	_, ctx := testContext(t)
   442  	nodes := []*structs.Node{
   443  		mock.Node(),
   444  		mock.Node(),
   445  		mock.Node(),
   446  	}
   447  	static := NewStaticIterator(ctx, nodes)
   448  
   449  	// Create a job with a distinct_hosts constraint and two task groups.
   450  	tg1 := &structs.TaskGroup{Name: "bar"}
   451  	tg2 := &structs.TaskGroup{Name: "baz"}
   452  
   453  	job := &structs.Job{
   454  		ID:          "foo",
   455  		Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}},
   456  		TaskGroups:  []*structs.TaskGroup{tg1, tg2},
   457  	}
   458  
   459  	// Add allocs placing tg1 on node1 and tg2 on node2. This should make the
   460  	// job unsatisfiable on all nodes but node3
   461  	plan := ctx.Plan()
   462  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   463  		&structs.Allocation{
   464  			TaskGroup: tg1.Name,
   465  			JobID:     job.ID,
   466  			ID:        structs.GenerateUUID(),
   467  		},
   468  
   469  		// Should be ignored as it is a different job.
   470  		&structs.Allocation{
   471  			TaskGroup: tg2.Name,
   472  			JobID:     "ignore 2",
   473  			ID:        structs.GenerateUUID(),
   474  		},
   475  	}
   476  	plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{
   477  		&structs.Allocation{
   478  			TaskGroup: tg2.Name,
   479  			JobID:     job.ID,
   480  			ID:        structs.GenerateUUID(),
   481  		},
   482  
   483  		// Should be ignored as it is a different job.
   484  		&structs.Allocation{
   485  			TaskGroup: tg1.Name,
   486  			JobID:     "ignore 2",
   487  			ID:        structs.GenerateUUID(),
   488  		},
   489  	}
   490  
   491  	proposed := NewDistinctHostsIterator(ctx, static)
   492  	proposed.SetTaskGroup(tg1)
   493  	proposed.SetJob(job)
   494  
   495  	out := collectFeasible(proposed)
   496  	if len(out) != 1 {
   497  		t.Fatalf("Bad: %#v", out)
   498  	}
   499  
   500  	if out[0].ID != nodes[2].ID {
   501  		t.Fatalf("wrong node picked")
   502  	}
   503  }
   504  
   505  func TestDistinctHostsIterator_JobDistinctHosts_InfeasibleCount(t *testing.T) {
   506  	_, ctx := testContext(t)
   507  	nodes := []*structs.Node{
   508  		mock.Node(),
   509  		mock.Node(),
   510  	}
   511  	static := NewStaticIterator(ctx, nodes)
   512  
   513  	// Create a job with a distinct_hosts constraint and three task groups.
   514  	tg1 := &structs.TaskGroup{Name: "bar"}
   515  	tg2 := &structs.TaskGroup{Name: "baz"}
   516  	tg3 := &structs.TaskGroup{Name: "bam"}
   517  
   518  	job := &structs.Job{
   519  		ID:          "foo",
   520  		Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}},
   521  		TaskGroups:  []*structs.TaskGroup{tg1, tg2, tg3},
   522  	}
   523  
   524  	// Add allocs placing tg1 on node1 and tg2 on node2. This should make the
   525  	// job unsatisfiable for tg3
   526  	plan := ctx.Plan()
   527  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   528  		&structs.Allocation{
   529  			TaskGroup: tg1.Name,
   530  			JobID:     job.ID,
   531  			ID:        structs.GenerateUUID(),
   532  		},
   533  	}
   534  	plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{
   535  		&structs.Allocation{
   536  			TaskGroup: tg2.Name,
   537  			JobID:     job.ID,
   538  			ID:        structs.GenerateUUID(),
   539  		},
   540  	}
   541  
   542  	proposed := NewDistinctHostsIterator(ctx, static)
   543  	proposed.SetTaskGroup(tg3)
   544  	proposed.SetJob(job)
   545  
   546  	// It should not be able to place 3 tasks with only two nodes.
   547  	out := collectFeasible(proposed)
   548  	if len(out) != 0 {
   549  		t.Fatalf("Bad: %#v", out)
   550  	}
   551  }
   552  
   553  func TestDistinctHostsIterator_TaskGroupDistinctHosts(t *testing.T) {
   554  	_, ctx := testContext(t)
   555  	nodes := []*structs.Node{
   556  		mock.Node(),
   557  		mock.Node(),
   558  	}
   559  	static := NewStaticIterator(ctx, nodes)
   560  
   561  	// Create a task group with a distinct_hosts constraint.
   562  	tg1 := &structs.TaskGroup{
   563  		Name: "example",
   564  		Constraints: []*structs.Constraint{
   565  			{Operand: structs.ConstraintDistinctHosts},
   566  		},
   567  	}
   568  	tg2 := &structs.TaskGroup{Name: "baz"}
   569  
   570  	// Add a planned alloc to node1.
   571  	plan := ctx.Plan()
   572  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   573  		&structs.Allocation{
   574  			TaskGroup: tg1.Name,
   575  			JobID:     "foo",
   576  		},
   577  	}
   578  
   579  	// Add a planned alloc to node2 with the same task group name but a
   580  	// different job.
   581  	plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{
   582  		&structs.Allocation{
   583  			TaskGroup: tg1.Name,
   584  			JobID:     "bar",
   585  		},
   586  	}
   587  
   588  	proposed := NewDistinctHostsIterator(ctx, static)
   589  	proposed.SetTaskGroup(tg1)
   590  	proposed.SetJob(&structs.Job{ID: "foo"})
   591  
   592  	out := collectFeasible(proposed)
   593  	if len(out) != 1 {
   594  		t.Fatalf("Bad: %#v", out)
   595  	}
   596  
   597  	// Expect it to skip the first node as there is a previous alloc on it for
   598  	// the same task group.
   599  	if out[0] != nodes[1] {
   600  		t.Fatalf("Bad: %v", out)
   601  	}
   602  
   603  	// Since the other task group doesn't have the constraint, both nodes should
   604  	// be feasible.
   605  	proposed.Reset()
   606  	proposed.SetTaskGroup(tg2)
   607  	out = collectFeasible(proposed)
   608  	if len(out) != 2 {
   609  		t.Fatalf("Bad: %#v", out)
   610  	}
   611  }
   612  
   613  // This test puts creates allocations across task groups that use a property
   614  // value to detect if the constraint at the job level properly considers all
   615  // task groups.
   616  func TestDistinctPropertyIterator_JobDistinctProperty(t *testing.T) {
   617  	state, ctx := testContext(t)
   618  	nodes := []*structs.Node{
   619  		mock.Node(),
   620  		mock.Node(),
   621  		mock.Node(),
   622  		mock.Node(),
   623  		mock.Node(),
   624  	}
   625  
   626  	for i, n := range nodes {
   627  		n.Meta["rack"] = fmt.Sprintf("%d", i)
   628  
   629  		// Add to state store
   630  		if err := state.UpsertNode(uint64(100+i), n); err != nil {
   631  			t.Fatalf("failed to upsert node: %v", err)
   632  		}
   633  	}
   634  
   635  	static := NewStaticIterator(ctx, nodes)
   636  
   637  	// Create a job with a distinct_property constraint and a task groups.
   638  	tg1 := &structs.TaskGroup{Name: "bar"}
   639  	tg2 := &structs.TaskGroup{Name: "baz"}
   640  
   641  	job := &structs.Job{
   642  		ID: "foo",
   643  		Constraints: []*structs.Constraint{
   644  			{
   645  				Operand: structs.ConstraintDistinctProperty,
   646  				LTarget: "${meta.rack}",
   647  			},
   648  		},
   649  		TaskGroups: []*structs.TaskGroup{tg1, tg2},
   650  	}
   651  
   652  	// Add allocs placing tg1 on node1 and 2 and tg2 on node3 and 4. This should make the
   653  	// job unsatisfiable on all nodes but node5. Also mix the allocations
   654  	// existing in the plan and the state store.
   655  	plan := ctx.Plan()
   656  	alloc1ID := structs.GenerateUUID()
   657  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   658  		&structs.Allocation{
   659  			TaskGroup: tg1.Name,
   660  			JobID:     job.ID,
   661  			ID:        alloc1ID,
   662  			NodeID:    nodes[0].ID,
   663  		},
   664  
   665  		// Should be ignored as it is a different job.
   666  		&structs.Allocation{
   667  			TaskGroup: tg2.Name,
   668  			JobID:     "ignore 2",
   669  			ID:        structs.GenerateUUID(),
   670  			NodeID:    nodes[0].ID,
   671  		},
   672  	}
   673  	plan.NodeAllocation[nodes[2].ID] = []*structs.Allocation{
   674  		&structs.Allocation{
   675  			TaskGroup: tg2.Name,
   676  			JobID:     job.ID,
   677  			ID:        structs.GenerateUUID(),
   678  			NodeID:    nodes[2].ID,
   679  		},
   680  
   681  		// Should be ignored as it is a different job.
   682  		&structs.Allocation{
   683  			TaskGroup: tg1.Name,
   684  			JobID:     "ignore 2",
   685  			ID:        structs.GenerateUUID(),
   686  			NodeID:    nodes[2].ID,
   687  		},
   688  	}
   689  
   690  	// Put an allocation on Node 5 but make it stopped in the plan
   691  	stoppingAllocID := structs.GenerateUUID()
   692  	plan.NodeUpdate[nodes[4].ID] = []*structs.Allocation{
   693  		&structs.Allocation{
   694  			TaskGroup: tg2.Name,
   695  			JobID:     job.ID,
   696  			ID:        stoppingAllocID,
   697  			NodeID:    nodes[4].ID,
   698  		},
   699  	}
   700  
   701  	upserting := []*structs.Allocation{
   702  		// Have one of the allocations exist in both the plan and the state
   703  		// store. This resembles an allocation update
   704  		&structs.Allocation{
   705  			TaskGroup: tg1.Name,
   706  			JobID:     job.ID,
   707  			ID:        alloc1ID,
   708  			EvalID:    structs.GenerateUUID(),
   709  			NodeID:    nodes[0].ID,
   710  		},
   711  
   712  		&structs.Allocation{
   713  			TaskGroup: tg1.Name,
   714  			JobID:     job.ID,
   715  			ID:        structs.GenerateUUID(),
   716  			EvalID:    structs.GenerateUUID(),
   717  			NodeID:    nodes[1].ID,
   718  		},
   719  
   720  		// Should be ignored as it is a different job.
   721  		&structs.Allocation{
   722  			TaskGroup: tg2.Name,
   723  			JobID:     "ignore 2",
   724  			ID:        structs.GenerateUUID(),
   725  			EvalID:    structs.GenerateUUID(),
   726  			NodeID:    nodes[1].ID,
   727  		},
   728  		&structs.Allocation{
   729  			TaskGroup: tg2.Name,
   730  			JobID:     job.ID,
   731  			ID:        structs.GenerateUUID(),
   732  			EvalID:    structs.GenerateUUID(),
   733  			NodeID:    nodes[3].ID,
   734  		},
   735  
   736  		// Should be ignored as it is a different job.
   737  		&structs.Allocation{
   738  			TaskGroup: tg1.Name,
   739  			JobID:     "ignore 2",
   740  			ID:        structs.GenerateUUID(),
   741  			EvalID:    structs.GenerateUUID(),
   742  			NodeID:    nodes[3].ID,
   743  		},
   744  		&structs.Allocation{
   745  			TaskGroup: tg2.Name,
   746  			JobID:     job.ID,
   747  			ID:        stoppingAllocID,
   748  			EvalID:    structs.GenerateUUID(),
   749  			NodeID:    nodes[4].ID,
   750  		},
   751  	}
   752  	if err := state.UpsertAllocs(1000, upserting); err != nil {
   753  		t.Fatalf("failed to UpsertAllocs: %v", err)
   754  	}
   755  
   756  	proposed := NewDistinctPropertyIterator(ctx, static)
   757  	proposed.SetJob(job)
   758  	proposed.SetTaskGroup(tg2)
   759  	proposed.Reset()
   760  
   761  	out := collectFeasible(proposed)
   762  	if len(out) != 1 {
   763  		t.Fatalf("Bad: %#v", out)
   764  	}
   765  	if out[0].ID != nodes[4].ID {
   766  		t.Fatalf("wrong node picked")
   767  	}
   768  }
   769  
   770  // This test checks that if a node has an allocation on it that gets stopped,
   771  // there is a plan to re-use that for a new allocation, that the next select
   772  // won't select that node.
   773  func TestDistinctPropertyIterator_JobDistinctProperty_RemoveAndReplace(t *testing.T) {
   774  	state, ctx := testContext(t)
   775  	nodes := []*structs.Node{
   776  		mock.Node(),
   777  	}
   778  
   779  	nodes[0].Meta["rack"] = "1"
   780  
   781  	// Add to state store
   782  	if err := state.UpsertNode(uint64(100), nodes[0]); err != nil {
   783  		t.Fatalf("failed to upsert node: %v", err)
   784  	}
   785  
   786  	static := NewStaticIterator(ctx, nodes)
   787  
   788  	// Create a job with a distinct_property constraint and a task groups.
   789  	tg1 := &structs.TaskGroup{Name: "bar"}
   790  	job := &structs.Job{
   791  		ID: "foo",
   792  		Constraints: []*structs.Constraint{
   793  			{
   794  				Operand: structs.ConstraintDistinctProperty,
   795  				LTarget: "${meta.rack}",
   796  			},
   797  		},
   798  		TaskGroups: []*structs.TaskGroup{tg1},
   799  	}
   800  
   801  	plan := ctx.Plan()
   802  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   803  		&structs.Allocation{
   804  			TaskGroup: tg1.Name,
   805  			JobID:     job.ID,
   806  			ID:        structs.GenerateUUID(),
   807  			NodeID:    nodes[0].ID,
   808  		},
   809  	}
   810  
   811  	stoppingAllocID := structs.GenerateUUID()
   812  	plan.NodeUpdate[nodes[0].ID] = []*structs.Allocation{
   813  		&structs.Allocation{
   814  			TaskGroup: tg1.Name,
   815  			JobID:     job.ID,
   816  			ID:        stoppingAllocID,
   817  			NodeID:    nodes[0].ID,
   818  		},
   819  	}
   820  
   821  	upserting := []*structs.Allocation{
   822  		&structs.Allocation{
   823  			TaskGroup: tg1.Name,
   824  			JobID:     job.ID,
   825  			ID:        stoppingAllocID,
   826  			EvalID:    structs.GenerateUUID(),
   827  			NodeID:    nodes[0].ID,
   828  		},
   829  	}
   830  	if err := state.UpsertAllocs(1000, upserting); err != nil {
   831  		t.Fatalf("failed to UpsertAllocs: %v", err)
   832  	}
   833  
   834  	proposed := NewDistinctPropertyIterator(ctx, static)
   835  	proposed.SetJob(job)
   836  	proposed.SetTaskGroup(tg1)
   837  	proposed.Reset()
   838  
   839  	out := collectFeasible(proposed)
   840  	if len(out) != 0 {
   841  		t.Fatalf("Bad: %#v", out)
   842  	}
   843  }
   844  
   845  // This test creates previous allocations selecting certain property values to
   846  // test if it detects infeasibility of property values correctly and picks the
   847  // only feasible one
   848  func TestDistinctPropertyIterator_JobDistinctProperty_Infeasible(t *testing.T) {
   849  	state, ctx := testContext(t)
   850  	nodes := []*structs.Node{
   851  		mock.Node(),
   852  		mock.Node(),
   853  	}
   854  
   855  	for i, n := range nodes {
   856  		n.Meta["rack"] = fmt.Sprintf("%d", i)
   857  
   858  		// Add to state store
   859  		if err := state.UpsertNode(uint64(100+i), n); err != nil {
   860  			t.Fatalf("failed to upsert node: %v", err)
   861  		}
   862  	}
   863  
   864  	static := NewStaticIterator(ctx, nodes)
   865  
   866  	// Create a job with a distinct_property constraint and a task groups.
   867  	tg1 := &structs.TaskGroup{Name: "bar"}
   868  	tg2 := &structs.TaskGroup{Name: "baz"}
   869  	tg3 := &structs.TaskGroup{Name: "bam"}
   870  
   871  	job := &structs.Job{
   872  		ID: "foo",
   873  		Constraints: []*structs.Constraint{
   874  			{
   875  				Operand: structs.ConstraintDistinctProperty,
   876  				LTarget: "${meta.rack}",
   877  			},
   878  		},
   879  		TaskGroups: []*structs.TaskGroup{tg1, tg2, tg3},
   880  	}
   881  
   882  	// Add allocs placing tg1 on node1 and tg2 on node2. This should make the
   883  	// job unsatisfiable for tg3.
   884  	plan := ctx.Plan()
   885  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   886  		&structs.Allocation{
   887  			TaskGroup: tg1.Name,
   888  			JobID:     job.ID,
   889  			ID:        structs.GenerateUUID(),
   890  			NodeID:    nodes[0].ID,
   891  		},
   892  	}
   893  	upserting := []*structs.Allocation{
   894  		&structs.Allocation{
   895  			TaskGroup: tg2.Name,
   896  			JobID:     job.ID,
   897  			ID:        structs.GenerateUUID(),
   898  			EvalID:    structs.GenerateUUID(),
   899  			NodeID:    nodes[1].ID,
   900  		},
   901  	}
   902  	if err := state.UpsertAllocs(1000, upserting); err != nil {
   903  		t.Fatalf("failed to UpsertAllocs: %v", err)
   904  	}
   905  
   906  	proposed := NewDistinctPropertyIterator(ctx, static)
   907  	proposed.SetJob(job)
   908  	proposed.SetTaskGroup(tg3)
   909  	proposed.Reset()
   910  
   911  	out := collectFeasible(proposed)
   912  	if len(out) != 0 {
   913  		t.Fatalf("Bad: %#v", out)
   914  	}
   915  }
   916  
   917  // This test creates previous allocations selecting certain property values to
   918  // test if it detects infeasibility of property values correctly and picks the
   919  // only feasible one when the constraint is at the task group.
   920  func TestDistinctPropertyIterator_TaskGroupDistinctProperty(t *testing.T) {
   921  	state, ctx := testContext(t)
   922  	nodes := []*structs.Node{
   923  		mock.Node(),
   924  		mock.Node(),
   925  		mock.Node(),
   926  	}
   927  
   928  	for i, n := range nodes {
   929  		n.Meta["rack"] = fmt.Sprintf("%d", i)
   930  
   931  		// Add to state store
   932  		if err := state.UpsertNode(uint64(100+i), n); err != nil {
   933  			t.Fatalf("failed to upsert node: %v", err)
   934  		}
   935  	}
   936  
   937  	static := NewStaticIterator(ctx, nodes)
   938  
   939  	// Create a job with a task group with the distinct_property constraint
   940  	tg1 := &structs.TaskGroup{
   941  		Name: "example",
   942  		Constraints: []*structs.Constraint{
   943  			{
   944  				Operand: structs.ConstraintDistinctProperty,
   945  				LTarget: "${meta.rack}",
   946  			},
   947  		},
   948  	}
   949  	tg2 := &structs.TaskGroup{Name: "baz"}
   950  
   951  	job := &structs.Job{
   952  		ID:         "foo",
   953  		TaskGroups: []*structs.TaskGroup{tg1, tg2},
   954  	}
   955  
   956  	// Add allocs placing tg1 on node1 and 2. This should make the
   957  	// job unsatisfiable on all nodes but node3. Also mix the allocations
   958  	// existing in the plan and the state store.
   959  	plan := ctx.Plan()
   960  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   961  		&structs.Allocation{
   962  			TaskGroup: tg1.Name,
   963  			JobID:     job.ID,
   964  			ID:        structs.GenerateUUID(),
   965  			NodeID:    nodes[0].ID,
   966  		},
   967  	}
   968  
   969  	// Put an allocation on Node 3 but make it stopped in the plan
   970  	stoppingAllocID := structs.GenerateUUID()
   971  	plan.NodeUpdate[nodes[2].ID] = []*structs.Allocation{
   972  		&structs.Allocation{
   973  			TaskGroup: tg1.Name,
   974  			JobID:     job.ID,
   975  			ID:        stoppingAllocID,
   976  			NodeID:    nodes[2].ID,
   977  		},
   978  	}
   979  
   980  	upserting := []*structs.Allocation{
   981  		&structs.Allocation{
   982  			TaskGroup: tg1.Name,
   983  			JobID:     job.ID,
   984  			ID:        structs.GenerateUUID(),
   985  			EvalID:    structs.GenerateUUID(),
   986  			NodeID:    nodes[1].ID,
   987  		},
   988  
   989  		// Should be ignored as it is a different job.
   990  		&structs.Allocation{
   991  			TaskGroup: tg1.Name,
   992  			JobID:     "ignore 2",
   993  			ID:        structs.GenerateUUID(),
   994  			EvalID:    structs.GenerateUUID(),
   995  			NodeID:    nodes[2].ID,
   996  		},
   997  
   998  		&structs.Allocation{
   999  			TaskGroup: tg1.Name,
  1000  			JobID:     job.ID,
  1001  			ID:        stoppingAllocID,
  1002  			EvalID:    structs.GenerateUUID(),
  1003  			NodeID:    nodes[2].ID,
  1004  		},
  1005  	}
  1006  	if err := state.UpsertAllocs(1000, upserting); err != nil {
  1007  		t.Fatalf("failed to UpsertAllocs: %v", err)
  1008  	}
  1009  
  1010  	proposed := NewDistinctPropertyIterator(ctx, static)
  1011  	proposed.SetJob(job)
  1012  	proposed.SetTaskGroup(tg1)
  1013  	proposed.Reset()
  1014  
  1015  	out := collectFeasible(proposed)
  1016  	if len(out) != 1 {
  1017  		t.Fatalf("Bad: %#v", out)
  1018  	}
  1019  	if out[0].ID != nodes[2].ID {
  1020  		t.Fatalf("wrong node picked")
  1021  	}
  1022  
  1023  	// Since the other task group doesn't have the constraint, both nodes should
  1024  	// be feasible.
  1025  	proposed.SetTaskGroup(tg2)
  1026  	proposed.Reset()
  1027  
  1028  	out = collectFeasible(proposed)
  1029  	if len(out) != 3 {
  1030  		t.Fatalf("Bad: %#v", out)
  1031  	}
  1032  }
  1033  
  1034  func collectFeasible(iter FeasibleIterator) (out []*structs.Node) {
  1035  	for {
  1036  		next := iter.Next()
  1037  		if next == nil {
  1038  			break
  1039  		}
  1040  		out = append(out, next)
  1041  	}
  1042  	return
  1043  }
  1044  
  1045  // mockFeasibilityChecker is a FeasibilityChecker that returns predetermined
  1046  // feasibility values.
  1047  type mockFeasibilityChecker struct {
  1048  	retVals []bool
  1049  	i       int
  1050  }
  1051  
  1052  func newMockFeasiblityChecker(values ...bool) *mockFeasibilityChecker {
  1053  	return &mockFeasibilityChecker{retVals: values}
  1054  }
  1055  
  1056  func (c *mockFeasibilityChecker) Feasible(*structs.Node) bool {
  1057  	if c.i >= len(c.retVals) {
  1058  		c.i++
  1059  		return false
  1060  	}
  1061  
  1062  	f := c.retVals[c.i]
  1063  	c.i++
  1064  	return f
  1065  }
  1066  
  1067  // calls returns how many times the checker was called.
  1068  func (c *mockFeasibilityChecker) calls() int { return c.i }
  1069  
  1070  func TestFeasibilityWrapper_JobIneligible(t *testing.T) {
  1071  	_, ctx := testContext(t)
  1072  	nodes := []*structs.Node{mock.Node()}
  1073  	static := NewStaticIterator(ctx, nodes)
  1074  	mocked := newMockFeasiblityChecker(false)
  1075  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil)
  1076  
  1077  	// Set the job to ineligible
  1078  	ctx.Eligibility().SetJobEligibility(false, nodes[0].ComputedClass)
  1079  
  1080  	// Run the wrapper.
  1081  	out := collectFeasible(wrapper)
  1082  
  1083  	if out != nil || mocked.calls() != 0 {
  1084  		t.Fatalf("bad: %#v %d", out, mocked.calls())
  1085  	}
  1086  }
  1087  
  1088  func TestFeasibilityWrapper_JobEscapes(t *testing.T) {
  1089  	_, ctx := testContext(t)
  1090  	nodes := []*structs.Node{mock.Node()}
  1091  	static := NewStaticIterator(ctx, nodes)
  1092  	mocked := newMockFeasiblityChecker(false)
  1093  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil)
  1094  
  1095  	// Set the job to escaped
  1096  	cc := nodes[0].ComputedClass
  1097  	ctx.Eligibility().job[cc] = EvalComputedClassEscaped
  1098  
  1099  	// Run the wrapper.
  1100  	out := collectFeasible(wrapper)
  1101  
  1102  	if out != nil || mocked.calls() != 1 {
  1103  		t.Fatalf("bad: %#v", out)
  1104  	}
  1105  
  1106  	// Ensure that the job status didn't change from escaped even though the
  1107  	// option failed.
  1108  	if status := ctx.Eligibility().JobStatus(cc); status != EvalComputedClassEscaped {
  1109  		t.Fatalf("job status is %v; want %v", status, EvalComputedClassEscaped)
  1110  	}
  1111  }
  1112  
  1113  func TestFeasibilityWrapper_JobAndTg_Eligible(t *testing.T) {
  1114  	_, ctx := testContext(t)
  1115  	nodes := []*structs.Node{mock.Node()}
  1116  	static := NewStaticIterator(ctx, nodes)
  1117  	jobMock := newMockFeasiblityChecker(true)
  1118  	tgMock := newMockFeasiblityChecker(false)
  1119  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
  1120  
  1121  	// Set the job to escaped
  1122  	cc := nodes[0].ComputedClass
  1123  	ctx.Eligibility().job[cc] = EvalComputedClassEligible
  1124  	ctx.Eligibility().SetTaskGroupEligibility(true, "foo", cc)
  1125  	wrapper.SetTaskGroup("foo")
  1126  
  1127  	// Run the wrapper.
  1128  	out := collectFeasible(wrapper)
  1129  
  1130  	if out == nil || tgMock.calls() != 0 {
  1131  		t.Fatalf("bad: %#v %v", out, tgMock.calls())
  1132  	}
  1133  }
  1134  
  1135  func TestFeasibilityWrapper_JobEligible_TgIneligible(t *testing.T) {
  1136  	_, ctx := testContext(t)
  1137  	nodes := []*structs.Node{mock.Node()}
  1138  	static := NewStaticIterator(ctx, nodes)
  1139  	jobMock := newMockFeasiblityChecker(true)
  1140  	tgMock := newMockFeasiblityChecker(false)
  1141  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
  1142  
  1143  	// Set the job to escaped
  1144  	cc := nodes[0].ComputedClass
  1145  	ctx.Eligibility().job[cc] = EvalComputedClassEligible
  1146  	ctx.Eligibility().SetTaskGroupEligibility(false, "foo", cc)
  1147  	wrapper.SetTaskGroup("foo")
  1148  
  1149  	// Run the wrapper.
  1150  	out := collectFeasible(wrapper)
  1151  
  1152  	if out != nil || tgMock.calls() != 0 {
  1153  		t.Fatalf("bad: %#v %v", out, tgMock.calls())
  1154  	}
  1155  }
  1156  
  1157  func TestFeasibilityWrapper_JobEligible_TgEscaped(t *testing.T) {
  1158  	_, ctx := testContext(t)
  1159  	nodes := []*structs.Node{mock.Node()}
  1160  	static := NewStaticIterator(ctx, nodes)
  1161  	jobMock := newMockFeasiblityChecker(true)
  1162  	tgMock := newMockFeasiblityChecker(true)
  1163  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
  1164  
  1165  	// Set the job to escaped
  1166  	cc := nodes[0].ComputedClass
  1167  	ctx.Eligibility().job[cc] = EvalComputedClassEligible
  1168  	ctx.Eligibility().taskGroups["foo"] =
  1169  		map[string]ComputedClassFeasibility{cc: EvalComputedClassEscaped}
  1170  	wrapper.SetTaskGroup("foo")
  1171  
  1172  	// Run the wrapper.
  1173  	out := collectFeasible(wrapper)
  1174  
  1175  	if out == nil || tgMock.calls() != 1 {
  1176  		t.Fatalf("bad: %#v %v", out, tgMock.calls())
  1177  	}
  1178  
  1179  	if e, ok := ctx.Eligibility().taskGroups["foo"][cc]; !ok || e != EvalComputedClassEscaped {
  1180  		t.Fatalf("bad: %v %v", e, ok)
  1181  	}
  1182  }