Support Check-And-Set deletion of config entries (#11419)
Implements #11372
This commit is contained in:
parent
2bcd5c42b9
commit
a620b6be2e
|
@ -0,0 +1,6 @@
|
|||
```release-note:improvement
|
||||
config: Support Check-And-Set (CAS) deletion of config entries
|
||||
```
|
||||
```release-note:improvement
|
||||
cli: Add `-cas` and `-modify-index` flags to the `consul config delete` command to support Check-And-Set (CAS) deletion of config entries
|
||||
```
|
|
@ -101,12 +101,27 @@ func (s *HTTPHandlers) configDelete(resp http.ResponseWriter, req *http.Request)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var reply struct{}
|
||||
// Check for cas value
|
||||
if casStr := req.URL.Query().Get("cas"); casStr != "" {
|
||||
casVal, err := strconv.ParseUint(casStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args.Op = structs.ConfigEntryDeleteCAS
|
||||
args.Entry.GetRaftIndex().ModifyIndex = casVal
|
||||
}
|
||||
|
||||
var reply structs.ConfigEntryDeleteResponse
|
||||
if err := s.agent.RPC("ConfigEntry.Delete", &args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
// Return the `deleted` boolean for CAS operations, but not normal deletions
|
||||
// to maintain backwards-compatibility with existing callers.
|
||||
if args.Op == structs.ConfigEntryDeleteCAS {
|
||||
return reply.Deleted, nil
|
||||
}
|
||||
return struct{}{}, nil
|
||||
}
|
||||
|
||||
// ConfigApply applies the given config entry update.
|
||||
|
|
|
@ -196,6 +196,88 @@ func TestConfig_Delete(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConfig_Delete_CAS(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
|
||||
a := NewTestAgent(t, "")
|
||||
defer a.Shutdown()
|
||||
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
|
||||
|
||||
// Create a config entry.
|
||||
entry := &structs.ServiceConfigEntry{
|
||||
Kind: structs.ServiceDefaults,
|
||||
Name: "foo",
|
||||
}
|
||||
var created bool
|
||||
require.NoError(a.RPC("ConfigEntry.Apply", &structs.ConfigEntryRequest{
|
||||
Datacenter: "dc1",
|
||||
Entry: entry,
|
||||
}, &created))
|
||||
require.True(created)
|
||||
|
||||
// Read it back to get its ModifyIndex.
|
||||
var out structs.ConfigEntryResponse
|
||||
require.NoError(a.RPC("ConfigEntry.Get", &structs.ConfigEntryQuery{
|
||||
Datacenter: "dc1",
|
||||
Kind: entry.Kind,
|
||||
Name: entry.Name,
|
||||
}, &out))
|
||||
require.NotNil(out.Entry)
|
||||
|
||||
modifyIndex := out.Entry.GetRaftIndex().ModifyIndex
|
||||
|
||||
t.Run("attempt to delete with an invalid index", func(t *testing.T) {
|
||||
req := httptest.NewRequest(
|
||||
"DELETE",
|
||||
fmt.Sprintf("/v1/config/%s/%s?cas=%d", entry.Kind, entry.Name, modifyIndex-1),
|
||||
nil,
|
||||
)
|
||||
rawRsp, err := a.srv.Config(httptest.NewRecorder(), req)
|
||||
require.NoError(err)
|
||||
|
||||
deleted, isBool := rawRsp.(bool)
|
||||
require.True(isBool, "response should be a boolean")
|
||||
require.False(deleted, "entry should not have been deleted")
|
||||
|
||||
// Verify it was not deleted.
|
||||
var out structs.ConfigEntryResponse
|
||||
require.NoError(a.RPC("ConfigEntry.Get", &structs.ConfigEntryQuery{
|
||||
Datacenter: "dc1",
|
||||
Kind: entry.Kind,
|
||||
Name: entry.Name,
|
||||
}, &out))
|
||||
require.NotNil(out.Entry)
|
||||
})
|
||||
|
||||
t.Run("attempt to delete with a valid index", func(t *testing.T) {
|
||||
req := httptest.NewRequest(
|
||||
"DELETE",
|
||||
fmt.Sprintf("/v1/config/%s/%s?cas=%d", entry.Kind, entry.Name, modifyIndex),
|
||||
nil,
|
||||
)
|
||||
rawRsp, err := a.srv.Config(httptest.NewRecorder(), req)
|
||||
require.NoError(err)
|
||||
|
||||
deleted, isBool := rawRsp.(bool)
|
||||
require.True(isBool, "response should be a boolean")
|
||||
require.True(deleted, "entry should have been deleted")
|
||||
|
||||
// Verify it was deleted.
|
||||
var out structs.ConfigEntryResponse
|
||||
require.NoError(a.RPC("ConfigEntry.Get", &structs.ConfigEntryQuery{
|
||||
Datacenter: "dc1",
|
||||
Kind: entry.Kind,
|
||||
Name: entry.Name,
|
||||
}, &out))
|
||||
require.Nil(out.Entry)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfig_Apply(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
|
|
|
@ -262,7 +262,7 @@ func (c *ConfigEntry) ListAll(args *structs.ConfigEntryListAllRequest, reply *st
|
|||
}
|
||||
|
||||
// Delete deletes a config entry.
|
||||
func (c *ConfigEntry) Delete(args *structs.ConfigEntryRequest, reply *struct{}) error {
|
||||
func (c *ConfigEntry) Delete(args *structs.ConfigEntryRequest, reply *structs.ConfigEntryDeleteResponse) error {
|
||||
if err := c.srv.validateEnterpriseRequest(args.Entry.GetEnterpriseMeta(), true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -294,11 +294,32 @@ func (c *ConfigEntry) Delete(args *structs.ConfigEntryRequest, reply *struct{})
|
|||
return acl.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// Only delete and delete-cas ops are supported. If the caller erroneously
|
||||
// sent something else, we assume they meant delete.
|
||||
switch args.Op {
|
||||
case structs.ConfigEntryDelete, structs.ConfigEntryDeleteCAS:
|
||||
default:
|
||||
args.Op = structs.ConfigEntryDelete
|
||||
_, err = c.srv.raftApply(structs.ConfigEntryRequestType, args)
|
||||
}
|
||||
|
||||
rsp, err := c.srv.raftApply(structs.ConfigEntryRequestType, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if args.Op == structs.ConfigEntryDeleteCAS {
|
||||
// In CAS deletions the FSM will return a boolean value indicating whether the
|
||||
// operation was successful.
|
||||
deleted, _ := rsp.(bool)
|
||||
reply.Deleted = deleted
|
||||
} else {
|
||||
// For non-CAS deletions any non-error result indicates a successful deletion.
|
||||
reply.Deleted = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveServiceConfig
|
||||
func (c *ConfigEntry) ResolveServiceConfig(args *structs.ServiceConfigRequest, reply *structs.ServiceConfigResponse) error {
|
||||
if err := c.srv.validateEnterpriseRequest(&args.EnterpriseMeta, false); err != nil {
|
||||
|
|
|
@ -676,6 +676,64 @@ func TestConfigEntry_Delete(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestConfigEntry_DeleteCAS(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
|
||||
dir, s := testServer(t)
|
||||
defer os.RemoveAll(dir)
|
||||
defer s.Shutdown()
|
||||
|
||||
codec := rpcClient(t, s)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s.RPC, "dc1")
|
||||
|
||||
// Create a simple config entry.
|
||||
entry := &structs.ServiceConfigEntry{
|
||||
Kind: structs.ServiceDefaults,
|
||||
Name: "foo",
|
||||
}
|
||||
state := s.fsm.State()
|
||||
require.NoError(state.EnsureConfigEntry(1, entry))
|
||||
|
||||
// Verify it's there.
|
||||
_, existing, err := state.ConfigEntry(nil, entry.Kind, entry.Name, nil)
|
||||
require.NoError(err)
|
||||
|
||||
// Send a delete CAS request with an invalid index.
|
||||
args := structs.ConfigEntryRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ConfigEntryDeleteCAS,
|
||||
}
|
||||
args.Entry = entry.Clone()
|
||||
args.Entry.GetRaftIndex().ModifyIndex = existing.GetRaftIndex().ModifyIndex - 1
|
||||
|
||||
var rsp structs.ConfigEntryDeleteResponse
|
||||
require.NoError(msgpackrpc.CallWithCodec(codec, "ConfigEntry.Delete", &args, &rsp))
|
||||
require.False(rsp.Deleted)
|
||||
|
||||
// Verify the entry was not deleted.
|
||||
_, existing, err = s.fsm.State().ConfigEntry(nil, structs.ServiceDefaults, "foo", nil)
|
||||
require.NoError(err)
|
||||
require.NotNil(existing)
|
||||
|
||||
// Restore the valid index and try again.
|
||||
args.Entry.GetRaftIndex().ModifyIndex = existing.GetRaftIndex().ModifyIndex
|
||||
|
||||
require.NoError(msgpackrpc.CallWithCodec(codec, "ConfigEntry.Delete", &args, &rsp))
|
||||
require.True(rsp.Deleted)
|
||||
|
||||
// Verify the entry was deleted.
|
||||
_, existing, err = s.fsm.State().ConfigEntry(nil, structs.ServiceDefaults, "foo", nil)
|
||||
require.NoError(err)
|
||||
require.Nil(existing)
|
||||
}
|
||||
|
||||
func TestConfigEntry_Delete_ACLDeny(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
|
|
|
@ -237,7 +237,7 @@ func TestReplication_ConfigEntries(t *testing.T) {
|
|||
Entry: entry,
|
||||
}
|
||||
|
||||
var out struct{}
|
||||
var out structs.ConfigEntryDeleteResponse
|
||||
require.NoError(t, s1.RPC("ConfigEntry.Delete", &arg, &out))
|
||||
}
|
||||
|
||||
|
|
|
@ -543,6 +543,14 @@ func (c *FSM) applyConfigEntryOperation(buf []byte, index uint64) interface{} {
|
|||
return err
|
||||
}
|
||||
return true
|
||||
case structs.ConfigEntryDeleteCAS:
|
||||
defer metrics.MeasureSinceWithLabels([]string{"fsm", "config_entry", req.Entry.GetKind()}, time.Now(),
|
||||
[]metrics.Label{{Name: "op", Value: "delete"}})
|
||||
deleted, err := c.state.DeleteConfigEntryCAS(index, req.Entry.GetRaftIndex().ModifyIndex, req.Entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return deleted
|
||||
case structs.ConfigEntryDelete:
|
||||
defer metrics.MeasureSinceWithLabels([]string{"fsm", "config_entry", req.Entry.GetKind()}, time.Now(),
|
||||
[]metrics.Label{{Name: "op", Value: "delete"}})
|
||||
|
|
|
@ -1352,6 +1352,57 @@ func TestFSM_ConfigEntry(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFSM_ConfigEntry_DeleteCAS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
|
||||
logger := testutil.Logger(t)
|
||||
fsm, err := New(nil, logger)
|
||||
require.NoError(err)
|
||||
|
||||
// Create a simple config entry and write it to the state store.
|
||||
entry := &structs.ServiceConfigEntry{
|
||||
Kind: structs.ServiceDefaults,
|
||||
Name: "global",
|
||||
}
|
||||
require.NoError(fsm.state.EnsureConfigEntry(1, entry))
|
||||
|
||||
// Raft index is populated by EnsureConfigEntry, hold on to it so that we can
|
||||
// restore it later.
|
||||
raftIndex := entry.RaftIndex
|
||||
require.NotZero(raftIndex.ModifyIndex)
|
||||
|
||||
// Attempt a CAS delete with an invalid index.
|
||||
entry = entry.Clone()
|
||||
entry.RaftIndex = structs.RaftIndex{
|
||||
ModifyIndex: 99,
|
||||
}
|
||||
req := &structs.ConfigEntryRequest{
|
||||
Op: structs.ConfigEntryDeleteCAS,
|
||||
Entry: entry,
|
||||
}
|
||||
buf, err := structs.Encode(structs.ConfigEntryRequestType, req)
|
||||
require.NoError(err)
|
||||
|
||||
// Expect to get boolean false back.
|
||||
rsp := fsm.Apply(makeLog(buf))
|
||||
didDelete, isBool := rsp.(bool)
|
||||
require.True(isBool)
|
||||
require.False(didDelete)
|
||||
|
||||
// Attempt a CAS delete with a valid index.
|
||||
entry.RaftIndex = raftIndex
|
||||
buf, err = structs.Encode(structs.ConfigEntryRequestType, req)
|
||||
require.NoError(err)
|
||||
|
||||
// Expect to get boolean true back.
|
||||
rsp = fsm.Apply(makeLog(buf))
|
||||
didDelete, isBool = rsp.(bool)
|
||||
require.True(isBool)
|
||||
require.True(didDelete)
|
||||
}
|
||||
|
||||
// This adapts another test by chunking the encoded data and then performing
|
||||
// out-of-order applies of half the logs. It then snapshots, restores to a new
|
||||
// FSM, and applies the rest. The goal is to verify that chunking snapshotting
|
||||
|
|
|
@ -242,6 +242,41 @@ func (s *Store) EnsureConfigEntryCAS(idx, cidx uint64, conf structs.ConfigEntry)
|
|||
return err == nil, err
|
||||
}
|
||||
|
||||
// DeleteConfigEntryCAS performs a check-and-set deletion of a config entry
|
||||
// with the given raft index. If the index is not specified, or is not equal
|
||||
// to the entry's current ModifyIndex then the call is a noop, otherwise the
|
||||
// normal deletion is performed.
|
||||
func (s *Store) DeleteConfigEntryCAS(idx, cidx uint64, conf structs.ConfigEntry) (bool, error) {
|
||||
tx := s.db.WriteTxn(idx)
|
||||
defer tx.Abort()
|
||||
|
||||
existing, err := tx.First(tableConfigEntries, indexID, newConfigEntryQuery(conf))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed config entry lookup: %s", err)
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if existing.(structs.ConfigEntry).GetRaftIndex().ModifyIndex != cidx {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := deleteConfigEntryTxn(
|
||||
tx,
|
||||
idx,
|
||||
conf.GetKind(),
|
||||
conf.GetName(),
|
||||
conf.GetEnterpriseMeta(),
|
||||
); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteConfigEntry(idx uint64, kind, name string, entMeta *structs.EnterpriseMeta) error {
|
||||
tx := s.db.WriteTxn(idx)
|
||||
defer tx.Abort()
|
||||
|
|
|
@ -125,6 +125,47 @@ func TestStore_ConfigEntryCAS(t *testing.T) {
|
|||
require.Equal(updated, config)
|
||||
}
|
||||
|
||||
func TestStore_ConfigEntry_DeleteCAS(t *testing.T) {
|
||||
require := require.New(t)
|
||||
s := testConfigStateStore(t)
|
||||
|
||||
entry := &structs.ProxyConfigEntry{
|
||||
Kind: structs.ProxyDefaults,
|
||||
Name: "global",
|
||||
Config: map[string]interface{}{
|
||||
"DestinationServiceName": "foo",
|
||||
},
|
||||
}
|
||||
|
||||
// Attempt to delete the entry before it exists.
|
||||
ok, err := s.DeleteConfigEntryCAS(1, 0, entry)
|
||||
require.NoError(err)
|
||||
require.False(ok)
|
||||
|
||||
// Create the entry.
|
||||
require.NoError(s.EnsureConfigEntry(1, entry))
|
||||
|
||||
// Attempt to delete with an invalid index.
|
||||
ok, err = s.DeleteConfigEntryCAS(2, 99, entry)
|
||||
require.NoError(err)
|
||||
require.False(ok)
|
||||
|
||||
// Entry should not be deleted.
|
||||
_, config, err := s.ConfigEntry(nil, entry.Kind, entry.Name, nil)
|
||||
require.NoError(err)
|
||||
require.NotNil(config)
|
||||
|
||||
// Attempt to delete with a valid index.
|
||||
ok, err = s.DeleteConfigEntryCAS(2, 1, entry)
|
||||
require.NoError(err)
|
||||
require.True(ok)
|
||||
|
||||
// Entry should be deleted.
|
||||
_, config, err = s.ConfigEntry(nil, entry.Kind, entry.Name, nil)
|
||||
require.NoError(err)
|
||||
require.Nil(config)
|
||||
}
|
||||
|
||||
func TestStore_ConfigEntry_UpdateOver(t *testing.T) {
|
||||
// This test uses ServiceIntentions because they are the only
|
||||
// kind that implements UpdateOver() at this time.
|
||||
|
|
|
@ -445,6 +445,7 @@ const (
|
|||
ConfigEntryUpsert ConfigEntryOp = "upsert"
|
||||
ConfigEntryUpsertCAS ConfigEntryOp = "upsert-cas"
|
||||
ConfigEntryDelete ConfigEntryOp = "delete"
|
||||
ConfigEntryDeleteCAS ConfigEntryOp = "delete-cas"
|
||||
)
|
||||
|
||||
// ConfigEntryRequest is used when creating/updating/deleting a ConfigEntry.
|
||||
|
@ -1121,3 +1122,7 @@ func validateConfigEntryMeta(meta map[string]string) error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type ConfigEntryDeleteResponse struct {
|
||||
Deleted bool
|
||||
}
|
||||
|
|
|
@ -468,20 +468,45 @@ func (conf *ConfigEntries) set(entry ConfigEntry, params map[string]string, w *W
|
|||
}
|
||||
|
||||
func (conf *ConfigEntries) Delete(kind string, name string, w *WriteOptions) (*WriteMeta, error) {
|
||||
_, wm, err := conf.delete(kind, name, nil, w)
|
||||
return wm, err
|
||||
}
|
||||
|
||||
// DeleteCAS performs a Check-And-Set deletion of the given config entry, and
|
||||
// returns true if it was successful. If the provided index no longer matches
|
||||
// the entry's ModifyIndex (i.e. it was modified by another process) then the
|
||||
// operation will fail and return false.
|
||||
func (conf *ConfigEntries) DeleteCAS(kind, name string, index uint64, w *WriteOptions) (bool, *WriteMeta, error) {
|
||||
return conf.delete(kind, name, map[string]string{"cas": strconv.FormatUint(index, 10)}, w)
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) delete(kind, name string, params map[string]string, w *WriteOptions) (bool, *WriteMeta, error) {
|
||||
if kind == "" || name == "" {
|
||||
return nil, fmt.Errorf("Both kind and name parameters must not be empty")
|
||||
return false, nil, fmt.Errorf("Both kind and name parameters must not be empty")
|
||||
}
|
||||
|
||||
r := conf.c.newRequest("DELETE", fmt.Sprintf("/v1/config/%s/%s", kind, name))
|
||||
r.setWriteOptions(w)
|
||||
for param, value := range params {
|
||||
r.params.Set(param, value)
|
||||
}
|
||||
|
||||
rtt, resp, err := conf.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return false, nil, err
|
||||
}
|
||||
defer closeResponseBody(resp)
|
||||
|
||||
if err := requireOK(resp); err != nil {
|
||||
return nil, err
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
||||
return false, nil, fmt.Errorf("Failed to read response: %v", err)
|
||||
}
|
||||
|
||||
res := strings.Contains(buf.String(), "true")
|
||||
wm := &WriteMeta{RequestTime: rtt}
|
||||
return wm, nil
|
||||
return res, wm, nil
|
||||
}
|
||||
|
|
|
@ -249,6 +249,37 @@ func TestAPI_ConfigEntries(t *testing.T) {
|
|||
})
|
||||
})
|
||||
|
||||
t.Run("CAS deletion", func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
entry := &ProxyConfigEntry{
|
||||
Kind: ProxyDefaults,
|
||||
Name: ProxyConfigGlobal,
|
||||
Config: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
|
||||
// Create a config entry.
|
||||
created, _, err := config_entries.Set(entry, nil)
|
||||
require.NoError(err)
|
||||
require.True(created, "entry should have been created")
|
||||
|
||||
// Read it back to get the ModifyIndex.
|
||||
result, _, err := config_entries.Get(entry.Kind, entry.Name, nil)
|
||||
require.NoError(err)
|
||||
require.NotNil(entry)
|
||||
|
||||
// Attempt a deletion with an invalid index.
|
||||
deleted, _, err := config_entries.DeleteCAS(entry.Kind, entry.Name, result.GetModifyIndex()-1, nil)
|
||||
require.NoError(err)
|
||||
require.False(deleted, "entry should not have been deleted")
|
||||
|
||||
// Attempt a deletion with a valid index.
|
||||
deleted, _, err = config_entries.DeleteCAS(entry.Kind, entry.Name, result.GetModifyIndex(), nil)
|
||||
require.NoError(err)
|
||||
require.True(deleted, "entry should have been deleted")
|
||||
})
|
||||
}
|
||||
|
||||
func runStep(t *testing.T, name string, fn func(t *testing.T)) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
|
@ -22,12 +23,21 @@ type cmd struct {
|
|||
|
||||
kind string
|
||||
name string
|
||||
cas bool
|
||||
modifyIndex uint64
|
||||
}
|
||||
|
||||
func (c *cmd) init() {
|
||||
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
|
||||
c.flags.StringVar(&c.kind, "kind", "", "The kind of configuration to delete.")
|
||||
c.flags.StringVar(&c.name, "name", "", "The name of configuration to delete.")
|
||||
c.flags.BoolVar(&c.cas, "cas", false,
|
||||
"Perform a Check-And-Set operation. Specifying this value also "+
|
||||
"requires the -modify-index flag to be set. The default value "+
|
||||
"is false.")
|
||||
c.flags.Uint64Var(&c.modifyIndex, "modify-index", 0,
|
||||
"Unsigned integer representing the ModifyIndex of the config entry. "+
|
||||
"This is used in combination with the -cas flag.")
|
||||
c.http = &flags.HTTPFlags{}
|
||||
flags.Merge(c.flags, c.http.ClientFlags())
|
||||
flags.Merge(c.flags, c.http.ServerFlags())
|
||||
|
@ -40,13 +50,8 @@ func (c *cmd) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
if c.kind == "" {
|
||||
c.UI.Error("Must specify the -kind parameter")
|
||||
return 1
|
||||
}
|
||||
|
||||
if c.name == "" {
|
||||
c.UI.Error("Must specify the -name parameter")
|
||||
if err := c.validateArgs(); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
|
@ -55,17 +60,50 @@ func (c *cmd) Run(args []string) int {
|
|||
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
|
||||
return 1
|
||||
}
|
||||
entries := client.ConfigEntries()
|
||||
|
||||
var deleted bool
|
||||
if c.cas {
|
||||
deleted, _, err = entries.DeleteCAS(c.kind, c.name, c.modifyIndex, nil)
|
||||
} else {
|
||||
_, err = entries.Delete(c.kind, c.name, nil)
|
||||
deleted = err == nil
|
||||
}
|
||||
|
||||
_, err = client.ConfigEntries().Delete(c.kind, c.name, nil)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error deleting config entry %s/%s: %v", c.kind, c.name, err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if !deleted {
|
||||
c.UI.Error(fmt.Sprintf("Config entry not deleted: %s/%s", c.kind, c.name))
|
||||
return 1
|
||||
}
|
||||
|
||||
c.UI.Info(fmt.Sprintf("Config entry deleted: %s/%s", c.kind, c.name))
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *cmd) validateArgs() error {
|
||||
if c.kind == "" {
|
||||
return errors.New("Must specify the -kind parameter")
|
||||
}
|
||||
|
||||
if c.name == "" {
|
||||
return errors.New("Must specify the -name parameter")
|
||||
}
|
||||
|
||||
if c.cas && c.modifyIndex == 0 {
|
||||
return errors.New("Must specify a -modify-index greater than 0 with -cas")
|
||||
}
|
||||
|
||||
if c.modifyIndex != 0 && !c.cas {
|
||||
return errors.New("Cannot specify -modify-index without -cas")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cmd) Synopsis() string {
|
||||
return synopsis
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/consul/agent"
|
||||
|
@ -53,12 +54,97 @@ func TestConfigDelete(t *testing.T) {
|
|||
require.Nil(t, entry)
|
||||
}
|
||||
|
||||
func TestConfigDelete_CAS(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
|
||||
a := agent.NewTestAgent(t, ``)
|
||||
defer a.Shutdown()
|
||||
client := a.Client()
|
||||
|
||||
_, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{
|
||||
Kind: api.ServiceDefaults,
|
||||
Name: "web",
|
||||
Protocol: "tcp",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("with an invalid modify index", func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
c := New(ui)
|
||||
|
||||
args := []string{
|
||||
"-http-addr=" + a.HTTPAddr(),
|
||||
"-kind=" + api.ServiceDefaults,
|
||||
"-name=web",
|
||||
"-cas",
|
||||
"-modify-index=" + strconv.FormatUint(entry.GetModifyIndex()-1, 10),
|
||||
}
|
||||
|
||||
code := c.Run(args)
|
||||
require.Equal(t, 1, code)
|
||||
require.Contains(t, ui.ErrorWriter.String(),
|
||||
"Config entry not deleted: service-defaults/web")
|
||||
require.Empty(t, ui.OutputWriter.String())
|
||||
})
|
||||
|
||||
t.Run("with a valid modify index", func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
c := New(ui)
|
||||
|
||||
args := []string{
|
||||
"-http-addr=" + a.HTTPAddr(),
|
||||
"-kind=" + api.ServiceDefaults,
|
||||
"-name=web",
|
||||
"-cas",
|
||||
"-modify-index=" + strconv.FormatUint(entry.GetModifyIndex(), 10),
|
||||
}
|
||||
|
||||
code := c.Run(args)
|
||||
require.Equal(t, 0, code)
|
||||
require.Contains(t, ui.OutputWriter.String(),
|
||||
"Config entry deleted: service-defaults/web")
|
||||
require.Empty(t, ui.ErrorWriter.String())
|
||||
|
||||
entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, entry)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigDelete_InvalidArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := map[string][]string{
|
||||
"no kind": {},
|
||||
"no name": {"-kind", "service-defaults"},
|
||||
cases := map[string]struct {
|
||||
args []string
|
||||
err string
|
||||
}{
|
||||
"no kind": {
|
||||
args: []string{},
|
||||
err: "Must specify the -kind parameter",
|
||||
},
|
||||
"no name": {
|
||||
args: []string{"-kind", api.ServiceDefaults},
|
||||
err: "Must specify the -name parameter",
|
||||
},
|
||||
"cas but no modify-index": {
|
||||
args: []string{"-kind", api.ServiceDefaults, "-name", "web", "-cas"},
|
||||
err: "Must specify a -modify-index greater than 0 with -cas",
|
||||
},
|
||||
"cas but no zero modify-index": {
|
||||
args: []string{"-kind", api.ServiceDefaults, "-name", "web", "-cas", "-modify-index", "0"},
|
||||
err: "Must specify a -modify-index greater than 0 with -cas",
|
||||
},
|
||||
"modify-index but no cas": {
|
||||
args: []string{"-kind", api.ServiceDefaults, "-name", "web", "-modify-index", "1"},
|
||||
err: "Cannot specify -modify-index without -cas",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tcase := range cases {
|
||||
|
@ -66,8 +152,8 @@ func TestConfigDelete_InvalidArgs(t *testing.T) {
|
|||
ui := cli.NewMockUi()
|
||||
c := New(ui)
|
||||
|
||||
require.NotEqual(t, 0, c.Run(tcase))
|
||||
require.NotEmpty(t, ui.ErrorWriter.String())
|
||||
require.NotEqual(t, 0, c.Run(tcase.args))
|
||||
require.Contains(t, ui.ErrorWriter.String(), tcase.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -182,7 +182,7 @@ func WaitForServiceIntentions(t *testing.T, rpc rpcFn, dc string) {
|
|||
Name: fakeConfigName,
|
||||
},
|
||||
}
|
||||
var ignored struct{}
|
||||
var ignored structs.ConfigEntryDeleteResponse
|
||||
if err := rpc("ConfigEntry.Delete", args, &ignored); err != nil {
|
||||
r.Fatalf("err: %v", err)
|
||||
}
|
||||
|
|
|
@ -276,6 +276,12 @@ The table below shows this endpoint's support for
|
|||
`X-Consul-Namespace` header. If not provided, the namespace will be inherited
|
||||
from the request's ACL token or will default to the `default` namespace. Added in Consul 1.7.0.
|
||||
|
||||
- `cas` `(int: 0)` - Specifies to use a Check-And-Set operation. Unlike `PUT`,
|
||||
the index must be greater than 0 for Consul to take any action: a 0 index will
|
||||
not delete the config entry. If the index is non-zero, the config entry is only
|
||||
deleted if the index matches the `ModifyIndex` of that config entry. This is
|
||||
specified as part of the URL as a query parameter.
|
||||
|
||||
### Sample Request
|
||||
|
||||
```shell-session
|
||||
|
|
|
@ -31,6 +31,14 @@ Usage: `consul config delete [options]`
|
|||
`proxy-defaults` config entry must be `global`, and the name of the `mesh`
|
||||
config entry must be `mesh`.
|
||||
|
||||
- `-cas` - Perform a Check-And-Set operation. Specifying this value also
|
||||
requires the -modify-index flag to be set. The default value is false.
|
||||
|
||||
- `-modify-index=<int>` - Unsigned integer representing the ModifyIndex of the
|
||||
config entry. This is used in combination with the -cas flag.
|
||||
|
||||
## Examples
|
||||
|
||||
$ consul config delete -kind service-defaults -name web
|
||||
|
||||
$ consul config delete -kind service-defaults -name web -cas -modify-index 26
|
||||
|
|
Loading…
Reference in New Issue