github.com/hernad/nomad@v1.6.112/e2e/workload_id/nodemeta_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package workload_id
     5  
     6  import (
     7  	"os"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/hernad/nomad/api"
    12  	"github.com/hernad/nomad/e2e/e2eutil"
    13  	"github.com/hernad/nomad/helper/pointer"
    14  	"github.com/hernad/nomad/helper/uuid"
    15  	"github.com/hernad/nomad/jobspec2"
    16  	"github.com/shoenig/test/must"
    17  )
    18  
    19  // TestDynamicNodeMetadata runs subtests exercising the Dynamic Node Metadata
    20  // API. Bundled with Workload Identity as it is expected to be most used by
    21  // jobs via Task API + Workload Identity.
    22  func TestDynamicNodeMetadata(t *testing.T) {
    23  	nomad := e2eutil.NomadClient(t)
    24  
    25  	e2eutil.WaitForLeader(t, nomad)
    26  	e2eutil.WaitForNodesReady(t, nomad, 1)
    27  
    28  	t.Run("testDynamicNodeMetadata", testDynamicNodeMetadata)
    29  }
    30  
    31  // testDynamicNodeMetadata dynamically updates metadata on a node, schedules a
    32  // job using that metadata, and has the job update that metadata.
    33  func testDynamicNodeMetadata(t *testing.T) {
    34  	nomad := e2eutil.NomadClient(t)
    35  
    36  	nodes, err := e2eutil.ListLinuxClientNodes(nomad)
    37  	must.NoError(t, err)
    38  	if len(nodes) == 0 {
    39  		t.Skip("requires at least 1 linux node")
    40  	}
    41  
    42  	node, _, err := nomad.Nodes().Info(nodes[0], nil)
    43  	must.NoError(t, err)
    44  
    45  	keyFoo := "foo-" + uuid.Short()
    46  	keyEmpty := "empty-" + uuid.Short()
    47  	keyUnset := "unset-" + uuid.Short()
    48  
    49  	// Go ahead and submit job so it is scheduled as soon as the node metadata is
    50  	// applied
    51  	jobID := "node-meta-" + uuid.Short()
    52  	jobIDs := []string{jobID}
    53  	t.Cleanup(e2eutil.CleanupJobsAndGC(t, &jobIDs))
    54  
    55  	t.Logf("test config: job=%s node=%s foo=%s empty=%s unset=%s",
    56  		jobID, node.ID, keyFoo, keyEmpty, keyUnset)
    57  
    58  	path := "./input/node-meta.nomad.hcl"
    59  	jobBytes, err := os.ReadFile(path)
    60  	must.NoError(t, err)
    61  	job, err := jobspec2.ParseWithConfig(&jobspec2.ParseConfig{
    62  		Path: path,
    63  		Body: jobBytes,
    64  		ArgVars: []string{
    65  			"foo_constraint=${meta." + keyFoo + "}",
    66  			"empty_constraint=${meta." + keyEmpty + "}",
    67  			"unset_constraint=${meta." + keyUnset + "}",
    68  			"foo_key=" + keyFoo,
    69  			"empty_key=" + keyEmpty,
    70  			"unset_key=" + keyUnset,
    71  		},
    72  		Strict: true,
    73  	})
    74  	must.NoError(t, err)
    75  	job.ID = pointer.Of(jobID)
    76  
    77  	// Setup ACLs
    78  	for _, task := range job.TaskGroups[0].Tasks {
    79  		p := e2eutil.ApplyJobPolicy(t, nomad, "default",
    80  			jobID, *job.TaskGroups[0].Name, task.Name, `node { policy = "write" }`)
    81  
    82  		if p == nil {
    83  			t.Logf("skipping policy for %s as ACLs are disabled", task.Name)
    84  		} else {
    85  			t.Logf("created policy %s for %s", p.Name, task.Name)
    86  		}
    87  	}
    88  
    89  	// Register job
    90  	_, _, err = nomad.Jobs().Register(job, nil)
    91  	must.NoError(t, err)
    92  
    93  	// Update the node meta to allow the job to be placed
    94  	req := &api.NodeMetaApplyRequest{
    95  		NodeID: node.ID,
    96  		Meta: map[string]*string{
    97  			keyFoo:   pointer.Of("bar"),
    98  			keyEmpty: pointer.Of(""),
    99  			keyUnset: nil,
   100  		},
   101  	}
   102  	resp, err := nomad.Nodes().Meta().Apply(req, nil)
   103  	must.NoError(t, err)
   104  	must.Eq(t, "bar", resp.Meta[keyFoo])
   105  	must.MapContainsKey(t, resp.Meta, keyEmpty)
   106  	must.Eq(t, "", resp.Meta[keyEmpty])
   107  	must.MapNotContainsKey(t, resp.Meta, keyUnset)
   108  
   109  	t.Logf("job submitted, node metadata applied, waiting for metadata to be visible...")
   110  
   111  	// Wait up to 10 seconds (with 1s buffer) for updates to be visible to the
   112  	// scheduler.
   113  	qo := &api.QueryOptions{
   114  		AllowStale: false,
   115  		WaitIndex:  node.ModifyIndex,
   116  		WaitTime:   2 * time.Second,
   117  	}
   118  	deadline := time.Now().Add(11 * time.Second)
   119  	found := false
   120  	for !found && time.Now().Before(deadline) {
   121  		node, qm, err := nomad.Nodes().Info(node.ID, qo)
   122  		must.NoError(t, err)
   123  		qo.WaitIndex = qm.LastIndex
   124  		t.Logf("checking node at index %d", qm.LastIndex)
   125  
   126  		// This check races with the job scheduling, so only check keyFoo as the
   127  		// other 2 keys are manipulated by the job itself!
   128  		if node.Meta[keyFoo] == "bar" {
   129  			found = true
   130  		}
   131  	}
   132  	must.True(t, found, must.Sprintf("node %q did not update by deadline", node.ID))
   133  
   134  	// Wait for the job to complete
   135  	t.Logf("waiting for job %s to complete", jobID)
   136  	var alloc *api.AllocationListStub
   137  	deadline = time.Now().Add(1 * time.Minute)
   138  	found = false
   139  	for !found && time.Now().Before(deadline) {
   140  		allocs, _, err := nomad.Jobs().Allocations(jobID, true, nil)
   141  		must.NoError(t, err)
   142  		if len(allocs) > 0 {
   143  			for _, alloc = range allocs {
   144  				if alloc.ClientStatus == "complete" {
   145  					found = true
   146  					break
   147  				}
   148  			}
   149  		}
   150  		time.Sleep(100 * time.Millisecond)
   151  	}
   152  	must.True(t, found, must.Sprintf("did not find completed alloc"))
   153  
   154  	// Ensure the job's meta updates were applied
   155  	resp, err = nomad.Nodes().Meta().Read(node.ID, nil)
   156  	must.NoError(t, err)
   157  	must.Eq(t, "bar", resp.Meta[keyFoo])
   158  	must.Eq(t, "set", resp.Meta[keyUnset])
   159  	must.MapNotContainsKey(t, resp.Meta, keyEmpty)
   160  	must.MapContainsKey(t, resp.Dynamic, keyEmpty)
   161  	must.Nil(t, resp.Dynamic[keyEmpty])
   162  }