Files
nomad/api/variables_test.go
Juana De La Cuesta 72acaf6623 [17449] Introduces a locking mechanism over variables (#18207)
It includes the work over the state store, the PRC server, the HTTP server, the go API package and the CLI's  command. To read more on the actuall functionality, refer to the RFCs [NMD-178] Locking with Nomad Variables and [NMD-179] Leader election using locking mechanism for the Autoscaler.
2023-09-21 17:56:33 +02:00

373 lines
8.8 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package api
import (
"errors"
"fmt"
"testing"
"time"
"github.com/hashicorp/nomad/api/internal/testutil"
"github.com/shoenig/test/must"
)
func TestVariables_SimpleCRUD(t *testing.T) {
testutil.Parallel(t)
c, s := makeClient(t, nil, nil)
defer s.Stop()
nsv := c.Variables()
sv1 := NewVariable("my/first/variable/SimpleCRUD")
sv1.Namespace = "default"
sv1.Items["k1"] = "v1"
sv1.Items["k2"] = "v2"
sv2 := sv1.Copy()
sv2.Path = "other/variable/b"
sv2.Items["k1"] = "otherv1"
sv2.Items["k2"] = "otherv2"
t.Run("1 fail create when no items", func(t *testing.T) {
_, _, err := nsv.Create(&Variable{Path: "bad/var"}, nil)
must.ErrorContains(t, err, "Unexpected response code: 400 (variable missing required Items object)")
})
t.Run("2 create sv1", func(t *testing.T) {
get, _, err := nsv.Create(sv1, nil)
must.NoError(t, err)
must.NotNil(t, get)
must.Positive(t, get.CreateIndex)
must.Positive(t, get.CreateTime)
must.Positive(t, get.ModifyIndex)
must.Positive(t, get.ModifyTime)
must.Eq(t, sv1.Items, get.Items)
*sv1 = *get
})
t.Run("2 create sv2", func(t *testing.T) {
var err error
sv2, _, err = nsv.Create(sv2, nil)
must.NoError(t, err)
})
// TODO: Need to prevent no-op modifications from happening server-side
// t.Run("3 update sv1 no change", func(t *testing.T) {
// get, _, err := nsv.Update(sv1, nil)
// require.NoError(t, err)
// require.NotNil(t, get)
// require.Equal(t, sv1.ModifyIndex, get.ModifyIndex, "ModifyIndex should not change")
// require.Equal(t, sv1.Items, get.Items)
// *sv1 = *get
// })
t.Run("4 update sv1", func(t *testing.T) {
sv1.Items["new-hotness"] = "yeah!"
get, _, err := nsv.Update(sv1, nil)
must.NoError(t, err)
must.NotNil(t, get)
must.NotEq(t, sv1.ModifyIndex, get.ModifyIndex)
must.Eq(t, sv1.Items, get.Items)
*sv1 = *get
})
t.Run("5 list vars", func(t *testing.T) {
l, _, err := nsv.List(nil)
must.NoError(t, err)
must.Len(t, 2, l)
must.Eq(t, []*VariableMetadata{sv1.Metadata(), sv2.Metadata()}, l)
})
t.Run("5a list vars opts", func(t *testing.T) {
// Since there are two vars in the backend, we should
// get a NextToken with a page size of 1
l, qm, err := nsv.List(&QueryOptions{PerPage: 1})
must.NoError(t, err)
must.Len(t, 1, l)
must.Eq(t, sv1.Metadata(), l[0])
must.NotNil(t, qm.NextToken)
})
t.Run("5b prefixlist", func(t *testing.T) {
l, _, err := nsv.PrefixList("my", nil)
must.NoError(t, err)
must.Len(t, 1, l)
must.Eq(t, sv1.Metadata(), l[0])
})
t.Run("6 delete sv1", func(t *testing.T) {
_, err := nsv.Delete(sv1.Path, nil)
must.NoError(t, err)
_, _, err = nsv.Read(sv1.Path, nil)
must.ErrorIs(t, err, ErrVariablePathNotFound)
})
t.Run("7 list vars after delete", func(t *testing.T) {
l, _, err := nsv.List(nil)
must.NoError(t, err)
must.NotNil(t, l)
must.Len(t, 1, l)
})
}
func TestVariables_CRUDWithCAS(t *testing.T) {
testutil.Parallel(t)
c, s := makeClient(t, nil, nil)
defer s.Stop()
nsv := c.Variables()
sv1 := &Variable{
Path: "cas/variable/a",
Items: map[string]string{
"key1": "value1",
"key2": "value2",
},
}
// Create sv1: should pass without issue
get, _, err := nsv.Create(sv1, nil)
must.NoError(t, err)
must.NotNil(t, get)
must.Positive(t, get.CreateIndex)
must.Positive(t, get.CreateTime)
must.Positive(t, get.ModifyIndex)
must.Positive(t, get.ModifyTime)
must.Eq(t, sv1.Items, get.Items)
// Update sv1 with CAS:
// - perform out of band upsert
oobUpdate := sv1.Copy()
oobUpdate.Items["new-hotness"] = "yeah!"
nowVal, _, err := nsv.Update(oobUpdate, nil)
must.NoError(t, err)
// - try to do an update with sv1's old state; should fail
_, _, err = nsv.CheckedUpdate(sv1, nil)
must.Error(t, err)
// - expect the error to be an ErrCASConflict, so we can cast
// to it and retrieve the Conflict value
var conflictErr ErrCASConflict
must.True(t, errors.As(err, &conflictErr))
must.Eq(t, nowVal, conflictErr.Conflict)
// Delete CAS: try to delete sv1 at old ModifyIndex; should
// return an ErrCASConflict. Check Conflict.
_, err = nsv.CheckedDelete(sv1.Path, sv1.ModifyIndex, nil)
must.True(t, errors.As(err, &conflictErr))
must.Eq(t, nowVal, conflictErr.Conflict)
// Delete CAS: delete at the current index; should succeed.
_, err = nsv.CheckedDelete(sv1.Path, nowVal.ModifyIndex, nil)
must.NoError(t, err)
}
func TestVariables_Read(t *testing.T) {
testutil.Parallel(t)
c, s := makeClient(t, nil, nil)
defer s.Stop()
nsv := c.Variables()
tID := fmt.Sprint(time.Now().UTC().UnixNano())
sv1 := Variable{
Namespace: "default",
Path: tID + "/sv1",
Items: map[string]string{
"kv1": "val1",
"kv2": "val2",
},
}
writeTestVariable(t, c, &sv1)
testCases := []struct {
name string
path string
expectedError error
checkValue bool
expectedValue *Variable
}{
{
name: "not found",
path: tID + "/not/found",
expectedError: ErrVariablePathNotFound,
checkValue: true,
expectedValue: nil,
},
{
name: "found",
path: sv1.Path,
checkValue: true,
expectedValue: &sv1,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
get, _, err := nsv.Read(tc.path, nil)
if tc.expectedError != nil {
must.ErrorIs(t, err, tc.expectedError)
} else {
must.NoError(t, err)
}
if tc.checkValue {
if tc.expectedValue != nil {
must.NotNil(t, get)
must.Eq(t, tc.expectedValue, get)
} else {
must.Nil(t, get)
}
}
})
}
}
func TestVariables_GetVariableItems(t *testing.T) {
testutil.Parallel(t)
c, s := makeClient(t, nil, nil)
defer s.Stop()
nsv := c.Variables()
tID := fmt.Sprint(time.Now().UTC().UnixNano())
sv1 := Variable{
Namespace: "default",
Path: tID + "/sv1",
Items: map[string]string{
"kv1": "val1",
"kv2": "val2",
},
}
writeTestVariable(t, c, &sv1)
testCases := []struct {
name string
path string
expectedError error
checkValue bool
expectedValue VariableItems
}{
{
name: "not found",
path: tID + "/not/found",
expectedError: ErrVariablePathNotFound,
checkValue: true,
expectedValue: nil,
},
{
name: "found",
path: sv1.Path,
checkValue: true,
expectedValue: sv1.Items,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
get, _, err := nsv.GetVariableItems(tc.path, nil)
if tc.expectedError != nil {
must.ErrorIs(t, err, tc.expectedError)
} else {
must.NoError(t, err)
}
if tc.checkValue {
if tc.expectedValue != nil {
must.NotNil(t, get)
must.Eq(t, tc.expectedValue, get)
} else {
must.Nil(t, get)
}
}
})
}
}
func writeTestVariable(t *testing.T, c *Client, sv *Variable) {
_, err := c.put("/v1/var/"+sv.Path, sv, sv, nil)
must.NoError(t, err, must.Sprint("failed writing test variable"))
}
func TestVariable_CreateReturnsContent(t *testing.T) {
c, s := makeClient(t, nil, nil)
defer s.Stop()
nsv := c.Variables()
sv1 := NewVariable("my/first/variable/create")
sv1.Namespace = "default"
sv1.Items["k1"] = "v1"
sv1.Items["k2"] = "v2"
sv1n, _, err := nsv.Create(sv1, nil)
must.NoError(t, err)
must.NotNil(t, sv1n)
must.Eq(t, sv1.Items, sv1n.Items)
}
func TestVariables_LockRenewRelease(t *testing.T) {
testutil.Parallel(t)
c, s := makeClient(t, nil, nil)
defer s.Stop()
path := fmt.Sprintf("%s-%v", "/first/variable", time.Now().UnixMilli())
nsv := c.Variables()
sv1 := NewVariable(path)
sv1.Namespace = "default"
sv1.Items["k1"] = "v1"
sv1.Items["k2"] = "v2"
t.Run("1 create sv1", func(t *testing.T) {
get, _, err := nsv.Create(sv1, nil)
must.NoError(t, err)
must.NotNil(t, get)
must.Positive(t, get.CreateIndex)
must.Positive(t, get.CreateTime)
must.Positive(t, get.ModifyIndex)
must.Positive(t, get.ModifyTime)
must.Eq(t, sv1.Items, get.Items)
*sv1 = *get
})
t.Run("2 acquire lock on sv1", func(t *testing.T) {
get, _, err := nsv.AcquireLock(sv1, nil)
must.NoError(t, err)
must.NotNil(t, get)
must.NotEq(t, sv1.ModifyIndex, get.ModifyIndex)
must.Eq(t, sv1.Items, get.Items)
must.NotNil(t, get.Lock)
*sv1 = *get
})
t.Run("3 renew lock on sv1", func(t *testing.T) {
rlsv := *sv1
get, _, err := nsv.RenewLock(&rlsv, nil)
must.NoError(t, err)
must.NotNil(t, get)
must.Eq(t, sv1.ModifyIndex, get.ModifyIndex)
must.NotNil(t, get.Lock)
must.Eq(t, sv1.Lock.ID, get.Lock.ID)
})
t.Run("4 list vars", func(t *testing.T) {
l, _, err := nsv.List(nil)
must.NoError(t, err)
must.Len(t, 1, l)
must.Nil(t, l[0].Lock)
})
t.Run("5 release lock on sv1", func(t *testing.T) {
sv1.Items = nil
get, _, err := nsv.ReleaseLock(sv1, nil)
must.NoError(t, err)
must.NotNil(t, get)
must.NotEq(t, sv1.ModifyIndex, get.ModifyIndex)
must.Zero(t, len(get.Items))
must.Nil(t, get.Lock)
})
}