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 }