mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
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.
729 lines
23 KiB
Go
729 lines
23 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package agent
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/nomad/mock"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/shoenig/test/must"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var (
|
|
cb = func(c *Config) {
|
|
var ns int
|
|
ns = 0
|
|
c.LogLevel = "ERROR"
|
|
c.Server.NumSchedulers = &ns
|
|
}
|
|
)
|
|
|
|
func TestHTTP_Variables(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
httpTest(t, cb, func(s *TestAgent) {
|
|
// These tests are run against the same running server in order to reduce
|
|
// the costs of server startup and allow as much parallelization as possible.
|
|
t.Run("error_badverb_list", func(t *testing.T) {
|
|
req, err := http.NewRequest("LOLWUT", "/v1/vars", nil)
|
|
must.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
_, err = s.Server.VariablesListRequest(respW, req)
|
|
must.ErrorContains(t, err, ErrInvalidMethod)
|
|
})
|
|
t.Run("error_parse_list", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodGet, "/v1/vars?wait=99a", nil)
|
|
require.NoError(t, err)
|
|
|
|
respW := httptest.NewRecorder()
|
|
_, _ = s.Server.VariablesListRequest(respW, req)
|
|
must.Eq(t, http.StatusBadRequest, respW.Code)
|
|
must.Eq(t, "Invalid wait time", string(respW.Body.Bytes()))
|
|
})
|
|
t.Run("error_rpc_list", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodGet, "/v1/vars?region=bad", nil)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
obj, err := s.Server.VariablesListRequest(respW, req)
|
|
must.ErrorContains(t, err, "No path to region")
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("list", func(t *testing.T) {
|
|
// Test the empty list case
|
|
req, err := http.NewRequest(http.MethodGet, "/v1/vars", nil)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariablesListRequest(respW, req)
|
|
must.NoError(t, err)
|
|
|
|
// add vars and test a populated backend
|
|
svMap := mock.Variables(4, 4)
|
|
svs := svMap.List()
|
|
svs[3].Path = svs[0].Path + "/child"
|
|
for _, sv := range svs {
|
|
must.NoError(t, rpcWriteSV(s, sv, nil))
|
|
}
|
|
|
|
// Make the HTTP request
|
|
req, err = http.NewRequest(http.MethodGet, "/v1/vars", nil)
|
|
require.NoError(t, err)
|
|
respW = httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err = s.Server.VariablesListRequest(respW, req)
|
|
must.NoError(t, err)
|
|
|
|
// Check for the index
|
|
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-Index")))
|
|
must.Eq(t, "true", respW.HeaderMap.Get("X-Nomad-KnownLeader"))
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-LastContact")))
|
|
|
|
// Check the output (the 4 we register )
|
|
must.Len(t, 4, obj.([]*structs.VariableMetadata))
|
|
|
|
// test prefix query
|
|
req, err = http.NewRequest(http.MethodGet, "/v1/vars?prefix="+svs[0].Path, nil)
|
|
require.NoError(t, err)
|
|
respW = httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err = s.Server.VariablesListRequest(respW, req)
|
|
must.NoError(t, err)
|
|
must.Len(t, 2, obj.([]*structs.VariableMetadata))
|
|
})
|
|
rpcResetSV(s)
|
|
|
|
t.Run("error_badverb_query", func(t *testing.T) {
|
|
req, err := http.NewRequest("LOLWUT", "/v1/var/does/not/exist", nil)
|
|
must.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, ErrInvalidMethod)
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("error_parse_query", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodGet, "/v1/var/does/not/exist?wait=99a", nil)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
_, _ = s.Server.VariableSpecificRequest(respW, req)
|
|
must.Eq(t, http.StatusBadRequest, respW.Code)
|
|
must.Eq(t, "Invalid wait time", string(respW.Body.Bytes()))
|
|
})
|
|
t.Run("error_rpc_query", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodGet, "/v1/var/does/not/exist?region=bad", nil)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "No path to region")
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("query_unset_path", func(t *testing.T) {
|
|
// Make a request for a non-existing variable
|
|
req, err := http.NewRequest(http.MethodGet, "/v1/var/", nil)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "missing variable path")
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("query_unset_variable", func(t *testing.T) {
|
|
// Make a request for a non-existing variable
|
|
req, err := http.NewRequest(http.MethodGet, "/v1/var/not/real", nil)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "variable not found")
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("query", func(t *testing.T) {
|
|
// Use RPC to make a test variable
|
|
out := new(structs.VariableDecrypted)
|
|
sv1 := mock.Variable()
|
|
must.NoError(t, rpcWriteSV(s, sv1, out))
|
|
|
|
// Query a variable
|
|
req, err := http.NewRequest(http.MethodGet, "/v1/var/"+sv1.Path, nil)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.NoError(t, err)
|
|
|
|
// Check for the index
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-Index")))
|
|
must.Eq(t, "true", respW.HeaderMap.Get("X-Nomad-KnownLeader"))
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-LastContact")))
|
|
|
|
// Check the output
|
|
must.Eq(t, out, obj.(*structs.VariableDecrypted))
|
|
})
|
|
rpcResetSV(s)
|
|
|
|
sv1 := mock.Variable()
|
|
t.Run("error_parse_create", func(t *testing.T) {
|
|
buf := encodeBrokenReq(&sv1)
|
|
req, err := http.NewRequest(http.MethodPut, "/v1/var/"+sv1.Path, buf)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "unexpected EOF")
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("error_rpc_create", func(t *testing.T) {
|
|
buf := encodeReq(sv1)
|
|
req, err := http.NewRequest(http.MethodPut, "/v1/var/does/not/exist?region=bad", buf)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "No path to region")
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("create_no_items", func(t *testing.T) {
|
|
sv2 := sv1.Copy()
|
|
sv2.Items = nil
|
|
buf := encodeReq(sv2)
|
|
req, err := http.NewRequest(http.MethodPut, "/v1/var/"+sv1.Path, buf)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "variable missing required Items object")
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("create", func(t *testing.T) {
|
|
buf := encodeReq(sv1)
|
|
req, err := http.NewRequest(http.MethodPut, "/v1/var/"+sv1.Path, buf)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.NoError(t, err)
|
|
|
|
// Test the returned object and rehydrate to a VariableDecrypted
|
|
must.NotNil(t, obj)
|
|
sv1, ok := obj.(*structs.VariableDecrypted)
|
|
must.True(t, ok, must.Sprint(must.Sprint("Unable to convert obj to VariableDecrypted")))
|
|
|
|
// Check for the index
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-Index")))
|
|
must.Eq(t, fmt.Sprint(sv1.ModifyIndex), respW.HeaderMap.Get("X-Nomad-Index"))
|
|
|
|
// Check the variable was put and that the returned item matched the
|
|
// fetched value
|
|
out, err := rpcReadSV(s, sv1.Namespace, sv1.Path)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, out)
|
|
must.Eq(t, sv1, out)
|
|
})
|
|
rpcResetSV(s)
|
|
|
|
t.Run("error_parse_update", func(t *testing.T) {
|
|
sv1U := sv1.Copy()
|
|
sv1U.Items["new"] = "new"
|
|
|
|
// break the request body
|
|
badBuf := encodeBrokenReq(&sv1U)
|
|
|
|
req, err := http.NewRequest(http.MethodPut, "/v1/var/"+sv1.Path, badBuf)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "unexpected EOF")
|
|
|
|
var cErr HTTPCodedError
|
|
if !errors.As(err, &cErr) {
|
|
t.Fatalf("unexpected error")
|
|
}
|
|
must.Eq(t, http.StatusBadRequest, cErr.Code())
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("error_rpc_update", func(t *testing.T) {
|
|
sv1U := sv1.Copy()
|
|
sv1U.Items["new"] = "new"
|
|
|
|
// test broken rpc error
|
|
buf := encodeReq(&sv1U)
|
|
req, err := http.NewRequest(http.MethodPut, "/v1/var/"+sv1.Path+"?region=bad", buf)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "No path to region")
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("update", func(t *testing.T) {
|
|
sv := mock.Variable()
|
|
must.NoError(t, rpcWriteSV(s, sv, sv))
|
|
|
|
svU := sv.Copy()
|
|
svU.Items["new"] = "new"
|
|
// Make the HTTP request
|
|
buf := encodeReq(&svU)
|
|
req, err := http.NewRequest(http.MethodPut, "/v1/var/"+sv.Path, buf)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.NoError(t, err)
|
|
|
|
// Test the returned object and rehydrate to a VariableDecrypted
|
|
must.NotNil(t, obj)
|
|
out, ok := obj.(*structs.VariableDecrypted)
|
|
must.True(t, ok, must.Sprint("Unable to convert obj to VariableDecrypted"))
|
|
|
|
// Check for the index
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-Index")))
|
|
must.Eq(t, fmt.Sprint(out.ModifyIndex), respW.HeaderMap.Get("X-Nomad-Index"))
|
|
|
|
{
|
|
// Check that written varible does not equal the input to rule out input mutation
|
|
must.NotEqual(t, svU.VariableMetadata, out.VariableMetadata)
|
|
|
|
// Update the input token with the updated metadata so that we
|
|
// can use a simple equality check
|
|
svU.ModifyIndex = out.ModifyIndex
|
|
svU.ModifyTime = out.ModifyTime
|
|
must.Eq(t, &svU, out)
|
|
}
|
|
})
|
|
|
|
t.Run("update_cas", func(t *testing.T) {
|
|
sv := mock.Variable()
|
|
must.NoError(t, rpcWriteSV(s, sv, sv))
|
|
|
|
svU := sv.Copy()
|
|
svU.Items["new"] = "new"
|
|
|
|
// Make the HTTP request
|
|
{
|
|
buf := encodeReq(&svU)
|
|
req, err := http.NewRequest(http.MethodPut, "/v1/var/"+svU.Path+"?cas=1", buf)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.NoError(t, err)
|
|
must.Eq(t, http.StatusConflict, respW.Result().StatusCode)
|
|
|
|
// Evaluate the conflict variable
|
|
must.NotNil(t, obj)
|
|
conflict, ok := obj.(*structs.VariableDecrypted)
|
|
must.True(t, ok, must.Sprintf("Expected *structs.VariableDecrypted, got %T", obj))
|
|
must.Eq(t, conflict, sv)
|
|
|
|
// Check for the index
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-Index")))
|
|
}
|
|
// Check the variable was not updated
|
|
{
|
|
out, err := rpcReadSV(s, sv.Namespace, sv.Path)
|
|
must.NoError(t, err)
|
|
must.Eq(t, sv, out)
|
|
}
|
|
// Make the HTTP request
|
|
{
|
|
buf := encodeReq(&svU)
|
|
req, err := http.NewRequest(http.MethodPut, "/v1/var/"+svU.Path+"?cas="+fmt.Sprint(sv.ModifyIndex), buf)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.NoError(t, err)
|
|
|
|
// Test the returned object and rehydrate to a VariableDecrypted
|
|
must.NotNil(t, obj)
|
|
sv1, ok := obj.(*structs.VariableDecrypted)
|
|
must.True(t, ok, must.Sprint("Unable to convert obj to VariableDecrypted"))
|
|
|
|
// Check for the index
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-Index")))
|
|
must.Eq(t, fmt.Sprint(sv1.ModifyIndex), respW.HeaderMap.Get("X-Nomad-Index"))
|
|
|
|
// Check the variable was put and that the returned item matched the
|
|
// fetched value
|
|
out, err := rpcReadSV(s, sv.Namespace, sv.Path)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, out)
|
|
must.Eq(t, sv1, out)
|
|
|
|
}
|
|
// Check the variable was created correctly
|
|
{
|
|
out, err := rpcReadSV(s, sv.Namespace, sv.Path)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, out)
|
|
|
|
must.NotEq(t, sv, out)
|
|
must.NotEqual(t, svU.VariableMetadata, out.VariableMetadata)
|
|
|
|
// Update the input token with the updated metadata so that we
|
|
// can use a simple equality check
|
|
svU.CreateIndex, svU.ModifyIndex = out.CreateIndex, out.ModifyIndex
|
|
svU.CreateTime, svU.ModifyTime = out.CreateTime, out.ModifyTime
|
|
must.Eq(t, svU.VariableMetadata, out.VariableMetadata)
|
|
|
|
// fmt writes sorted output of maps for testability.
|
|
must.Eq(t, fmt.Sprint(svU.Items), fmt.Sprint(out.Items))
|
|
}
|
|
})
|
|
|
|
t.Run("error_cas_and_acquire_lock", func(t *testing.T) {
|
|
svLA := sv1.Copy()
|
|
svLA.Items["new"] = "new"
|
|
|
|
// break the request body
|
|
badBuf := encodeBrokenReq(&svLA)
|
|
|
|
req, err := http.NewRequest("PUT", "/v1/var/"+sv1.Path+"?cas=1&"+acquireLockQueryParam, badBuf)
|
|
must.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "CAS can't be used with lock operations")
|
|
|
|
var cErr HTTPCodedError
|
|
if !errors.As(err, &cErr) {
|
|
t.Fatalf("unexpected error")
|
|
}
|
|
|
|
must.Eq(t, http.StatusBadRequest, cErr.Code())
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("error_parse_acquire_lock", func(t *testing.T) {
|
|
svLA := sv1.Copy()
|
|
svLA.Items["new"] = "new"
|
|
|
|
// break the request body
|
|
badBuf := encodeBrokenReq(&svLA)
|
|
|
|
req, err := http.NewRequest("PUT", "/v1/var/"+sv1.Path+"?"+acquireLockQueryParam, badBuf)
|
|
must.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "unexpected EOF")
|
|
var cErr HTTPCodedError
|
|
|
|
if !errors.As(err, &cErr) {
|
|
t.Fatalf("unexpected error")
|
|
}
|
|
|
|
must.Eq(t, http.StatusBadRequest, cErr.Code())
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("error_rpc_acquire_lock", func(t *testing.T) {
|
|
svLA := sv1.Copy()
|
|
svLA.Items["new"] = "new"
|
|
|
|
// test broken rpc error
|
|
buf := encodeReq(&svLA)
|
|
req, err := http.NewRequest("PUT", "/v1/var/"+sv1.Path+"?region=bad&"+acquireLockQueryParam, buf)
|
|
must.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "No path to region")
|
|
must.Nil(t, obj)
|
|
})
|
|
|
|
t.Run("acquire_lock", func(t *testing.T) {
|
|
svLA := sv1
|
|
|
|
svLA.Items["new"] = "new"
|
|
// Make the HTTP request
|
|
buf := encodeReq(&svLA)
|
|
req, err := http.NewRequest("PUT", "/v1/var/"+svLA.Path+"?"+acquireLockQueryParam, buf)
|
|
must.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.NoError(t, err)
|
|
|
|
// Test the returned object and rehydrate to a VariableDecrypted
|
|
must.NotNil(t, obj)
|
|
out, ok := obj.(*structs.VariableDecrypted)
|
|
must.True(t, ok, must.Sprint("Unable to convert obj to VariableDecrypted"))
|
|
|
|
// Check for the index
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-Index")))
|
|
must.Eq(t, fmt.Sprint(out.ModifyIndex), respW.HeaderMap.Get("X-Nomad-Index"))
|
|
|
|
// Check for the lock
|
|
must.NotNil(t, out.VariableMetadata.Lock)
|
|
must.NonZero(t, len(out.LockID()))
|
|
|
|
// Check that written varible includes the new items
|
|
must.Eq(t, svLA.Items, out.Items)
|
|
|
|
// Update the lock information for the following tests
|
|
sv1.VariableMetadata = out.VariableMetadata
|
|
})
|
|
|
|
t.Run("error_rpc_renew_lock", func(t *testing.T) {
|
|
svRL := sv1.Copy()
|
|
|
|
// test broken rpc error
|
|
buf := encodeReq(&svRL)
|
|
req, err := http.NewRequest("PUT", "/v1/var/"+sv1.Path+"?region=bad&"+renewLockQueryParam, buf)
|
|
must.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "No path to region")
|
|
must.Nil(t, obj)
|
|
})
|
|
|
|
t.Run("renew_lock", func(t *testing.T) {
|
|
svRL := sv1.Copy()
|
|
|
|
// Make the HTTP request
|
|
buf := encodeReq(&svRL)
|
|
req, err := http.NewRequest("PUT", "/v1/var/"+svRL.Path+"?"+renewLockQueryParam, buf)
|
|
must.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.NoError(t, err)
|
|
|
|
// Test the returned object and rehydrate to a VariableDecrypted
|
|
must.NotNil(t, obj)
|
|
out, ok := obj.(*structs.VariableMetadata)
|
|
must.True(t, ok, must.Sprint("Unable to convert obj to VariableDecrypted"))
|
|
|
|
// Check for the lock
|
|
must.NotNil(t, out.Lock)
|
|
must.Eq(t, sv1.LockID(), out.Lock.ID)
|
|
})
|
|
|
|
t.Run("release_lock", func(t *testing.T) {
|
|
svLR := *sv1
|
|
|
|
svLR.Items = nil
|
|
// Make the HTTP request
|
|
buf := encodeReq(&svLR)
|
|
|
|
req, err := http.NewRequest("PUT", "/v1/var/"+svLR.Path+"?"+releaseLockQueryParam, buf)
|
|
must.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.NoError(t, err)
|
|
|
|
// Test the returned object and rehydrate to a VariableDecrypted
|
|
must.NotNil(t, obj)
|
|
out, ok := obj.(*structs.VariableDecrypted)
|
|
must.True(t, ok, must.Sprint("Unable to convert obj to VariableDecrypted"))
|
|
|
|
// Check for the index
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-Index")))
|
|
must.Eq(t, fmt.Sprint(out.ModifyIndex), respW.HeaderMap.Get("X-Nomad-Index"))
|
|
|
|
// Check for the lock
|
|
must.Nil(t, out.VariableMetadata.Lock)
|
|
|
|
// Check that written variable is equal the input
|
|
must.Zero(t, len(out.Items))
|
|
|
|
// Remove the lock information from the mock variable for the following tests
|
|
sv1.VariableMetadata = out.VariableMetadata
|
|
})
|
|
|
|
t.Run("error_rpc_delete", func(t *testing.T) {
|
|
sv1 := mock.Variable()
|
|
must.NoError(t, rpcWriteSV(s, sv1, nil))
|
|
|
|
// Make the HTTP request
|
|
req, err := http.NewRequest(http.MethodDelete, "/v1/var/"+sv1.Path+"?region=bad", nil)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.ErrorContains(t, err, "No path to region")
|
|
must.Nil(t, obj)
|
|
})
|
|
t.Run("delete-cas", func(t *testing.T) {
|
|
sv := mock.Variable()
|
|
must.NoError(t, rpcWriteSV(s, sv, nil))
|
|
sv, err := rpcReadSV(s, sv.Namespace, sv.Path)
|
|
must.NoError(t, err)
|
|
|
|
// Make the HTTP request
|
|
{
|
|
req, err := http.NewRequest(http.MethodDelete, "/v1/var/"+sv.Path+"?cas=1", nil)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.NoError(t, err)
|
|
must.Eq(t, http.StatusConflict, respW.Result().StatusCode)
|
|
|
|
// Evaluate the conflict variable
|
|
must.NotNil(t, obj)
|
|
conflict, ok := obj.(*structs.VariableDecrypted)
|
|
must.True(t, ok, must.Sprintf("Expected *structs.VariableDecrypted, got %T", obj))
|
|
must.True(t, sv.Equal(*conflict))
|
|
|
|
// Check for the index
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-Index")))
|
|
}
|
|
|
|
// Check variable was not deleted
|
|
{
|
|
svChk, err := rpcReadSV(s, sv.Namespace, sv.Path)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, svChk)
|
|
must.Eq(t, sv, svChk)
|
|
}
|
|
// Make the HTTP request
|
|
{
|
|
req, err := http.NewRequest(http.MethodDelete, "/v1/var/"+sv.Path+"?cas="+fmt.Sprint(sv.ModifyIndex), nil)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.NoError(t, err)
|
|
must.Nil(t, obj)
|
|
}
|
|
// Check variable was deleted
|
|
{
|
|
svChk, err := rpcReadSV(s, sv.Namespace, sv.Path)
|
|
must.NoError(t, err)
|
|
must.Nil(t, svChk)
|
|
}
|
|
})
|
|
t.Run("delete", func(t *testing.T) {
|
|
sv1 := mock.Variable()
|
|
must.NoError(t, rpcWriteSV(s, sv1, nil))
|
|
|
|
// Make the HTTP request
|
|
req, err := http.NewRequest(http.MethodDelete, "/v1/var/"+sv1.Path, nil)
|
|
require.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
|
|
// Make the request
|
|
obj, err := s.Server.VariableSpecificRequest(respW, req)
|
|
must.NoError(t, err)
|
|
must.Nil(t, obj)
|
|
|
|
// Check for the index
|
|
must.NonZero(t, len(respW.HeaderMap.Get("X-Nomad-Index")))
|
|
must.Eq(t, http.StatusNoContent, respW.Result().StatusCode)
|
|
|
|
// Check variable was deleted
|
|
sv, err := rpcReadSV(s, sv1.Namespace, sv1.Path)
|
|
must.NoError(t, err)
|
|
must.Nil(t, sv)
|
|
})
|
|
|
|
// WIP
|
|
t.Run("error_parse_lock_acquire", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", "/v1/var/does/not/exist?wait=99a&lock=acquire", nil)
|
|
must.NoError(t, err)
|
|
respW := httptest.NewRecorder()
|
|
_, _ = s.Server.VariableSpecificRequest(respW, req)
|
|
must.Eq(t, http.StatusBadRequest, respW.Code)
|
|
must.Eq(t, "Invalid wait time", string(respW.Body.Bytes()))
|
|
})
|
|
})
|
|
}
|
|
|
|
// encodeBrokenReq is a test helper that damages input JSON in order to create
|
|
// a parsing error for testing error pathways.
|
|
func encodeBrokenReq(obj interface{}) io.ReadCloser {
|
|
// var buf *bytes.Buffer
|
|
// enc := json.NewEncoder(buf)
|
|
// enc.Encode(obj)
|
|
b, _ := json.Marshal(obj)
|
|
b = b[0 : len(b)-5] // strip newline and final }
|
|
return io.NopCloser(bytes.NewReader(b))
|
|
}
|
|
|
|
// rpcReadSV lets this test read a variable using the RPC endpoint
|
|
func rpcReadSV(s *TestAgent, ns, p string) (*structs.VariableDecrypted, error) {
|
|
checkArgs := structs.VariablesReadRequest{Path: p, QueryOptions: structs.QueryOptions{Namespace: ns, Region: "global"}}
|
|
var checkResp structs.VariablesReadResponse
|
|
err := s.Agent.RPC(structs.VariablesReadRPCMethod, &checkArgs, &checkResp)
|
|
return checkResp.Data, err
|
|
}
|
|
|
|
// rpcWriteSV lets this test write a variable using the RPC endpoint
|
|
func rpcWriteSV(s *TestAgent, sv, out *structs.VariableDecrypted) error {
|
|
|
|
args := structs.VariablesApplyRequest{
|
|
Op: structs.VarOpSet,
|
|
Var: sv,
|
|
WriteRequest: structs.WriteRequest{Namespace: sv.Namespace, Region: "global"},
|
|
}
|
|
|
|
var resp structs.VariablesApplyResponse
|
|
err := s.Agent.RPC(structs.VariablesApplyRPCMethod, &args, &resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out != nil {
|
|
*out = *resp.Output
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// rpcResetSV lists all the variables for every namespace in the global
|
|
// region and deletes them using the RPC endpoints
|
|
func rpcResetSV(s *TestAgent) {
|
|
var lArgs structs.VariablesListRequest
|
|
var lResp structs.VariablesListResponse
|
|
|
|
lArgs = structs.VariablesListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: "*",
|
|
Region: "global",
|
|
},
|
|
}
|
|
err := s.Agent.RPC(structs.VariablesListRPCMethod, &lArgs, &lResp)
|
|
must.NoError(s.T, err)
|
|
|
|
dArgs := structs.VariablesApplyRequest{
|
|
Op: structs.VarOpDelete,
|
|
Var: &structs.VariableDecrypted{},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
},
|
|
}
|
|
|
|
var dResp structs.VariablesApplyResponse
|
|
|
|
for _, v := range lResp.Data {
|
|
dArgs.Var.Path = v.Path
|
|
dArgs.Var.Namespace = v.Namespace
|
|
err := s.Agent.RPC(structs.VariablesApplyRPCMethod, &dArgs, &dResp)
|
|
must.NoError(s.T, err)
|
|
}
|
|
|
|
err = s.Agent.RPC(structs.VariablesListRPCMethod, &lArgs, &lResp)
|
|
must.NoError(s.T, err)
|
|
must.Eq(s.T, 0, len(lResp.Data))
|
|
}
|