feat(cli): enable to delete config entry from an input file (#13677)
* feat(cli): enable to delete config entry from an input file - A new flag to config delete to delete a config entry in a valid config file, e.g., config delete -filename intention-allow.hcl - Updated flag validation; -filename and -kind can't be set at the same time - Move decode config entry method from config_write.go to helpers.go for reusing ParseConfigEntry() - add changelog Co-authored-by: Dan Upton <daniel@floppy.co>
This commit is contained in:
parent
18aacf9b55
commit
2d99304762
|
@ -0,0 +1,4 @@
|
||||||
|
```release-note:feature
|
||||||
|
cli: A new flag for config delete to delete a config entry in a
|
||||||
|
valid config file, e.g., config delete -filename intention-allow.hcl
|
||||||
|
```
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/command/flags"
|
"github.com/hashicorp/consul/command/flags"
|
||||||
|
"github.com/hashicorp/consul/command/helpers"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,10 +26,13 @@ type cmd struct {
|
||||||
name string
|
name string
|
||||||
cas bool
|
cas bool
|
||||||
modifyIndex uint64
|
modifyIndex uint64
|
||||||
|
filename string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cmd) init() {
|
func (c *cmd) init() {
|
||||||
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
|
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
|
|
||||||
|
c.flags.StringVar(&c.filename, "filename", "", "The filename of the config entry to delete")
|
||||||
c.flags.StringVar(&c.kind, "kind", "", "The kind of configuration to delete.")
|
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.StringVar(&c.name, "name", "", "The name of configuration to delete.")
|
||||||
c.flags.BoolVar(&c.cas, "cas", false,
|
c.flags.BoolVar(&c.cas, "cas", false,
|
||||||
|
@ -55,6 +59,25 @@ func (c *cmd) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kind := c.kind
|
||||||
|
name := c.name
|
||||||
|
var err error
|
||||||
|
if c.filename != "" {
|
||||||
|
data, err := helpers.LoadDataSourceNoRaw(c.filename, nil)
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Failed to load data: %v", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := helpers.ParseConfigEntry(data)
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Failed to decode config entry input: %v", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
kind = entry.GetKind()
|
||||||
|
name = entry.GetName()
|
||||||
|
}
|
||||||
|
|
||||||
client, err := c.http.APIClient()
|
client, err := c.http.APIClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
|
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
|
||||||
|
@ -64,27 +87,51 @@ func (c *cmd) Run(args []string) int {
|
||||||
|
|
||||||
var deleted bool
|
var deleted bool
|
||||||
if c.cas {
|
if c.cas {
|
||||||
deleted, _, err = entries.DeleteCAS(c.kind, c.name, c.modifyIndex, nil)
|
deleted, _, err = entries.DeleteCAS(kind, name, c.modifyIndex, nil)
|
||||||
} else {
|
} else {
|
||||||
_, err = entries.Delete(c.kind, c.name, nil)
|
_, err = entries.Delete(kind, name, nil)
|
||||||
deleted = err == nil
|
deleted = err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.UI.Error(fmt.Sprintf("Error deleting config entry %s/%s: %v", c.kind, c.name, err))
|
c.UI.Error(fmt.Sprintf("Error deleting config entry %s/%s: %v", kind, name, err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if !deleted {
|
if !deleted {
|
||||||
c.UI.Error(fmt.Sprintf("Config entry not deleted: %s/%s", c.kind, c.name))
|
c.UI.Error(fmt.Sprintf("Config entry not deleted: %s/%s", kind, name))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
c.UI.Info(fmt.Sprintf("Config entry deleted: %s/%s", c.kind, c.name))
|
c.UI.Info(fmt.Sprintf("Config entry deleted: %s/%s", kind, name))
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cmd) validateArgs() error {
|
func (c *cmd) validateArgs() error {
|
||||||
|
count := 0
|
||||||
|
if c.filename != "" {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.kind != "" {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.name != "" {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if count >= 3 {
|
||||||
|
return errors.New("filename can't be used with kind or name")
|
||||||
|
} else if count == 0 {
|
||||||
|
return errors.New("Must specify the -kind or -filename parameter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.filename != "" {
|
||||||
|
if count == 2 {
|
||||||
|
return errors.New("filename can't be used with kind or name")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if c.kind == "" {
|
if c.kind == "" {
|
||||||
return errors.New("Must specify the -kind parameter")
|
return errors.New("Must specify the -kind parameter")
|
||||||
}
|
}
|
||||||
|
@ -92,6 +139,7 @@ func (c *cmd) validateArgs() error {
|
||||||
if c.name == "" {
|
if c.name == "" {
|
||||||
return errors.New("Must specify the -name parameter")
|
return errors.New("Must specify the -name parameter")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if c.cas && c.modifyIndex == 0 {
|
if c.cas && c.modifyIndex == 0 {
|
||||||
return errors.New("Must specify a -modify-index greater than 0 with -cas")
|
return errors.New("Must specify a -modify-index greater than 0 with -cas")
|
||||||
|
@ -115,12 +163,13 @@ func (c *cmd) Help() string {
|
||||||
const (
|
const (
|
||||||
synopsis = "Delete a centralized config entry"
|
synopsis = "Delete a centralized config entry"
|
||||||
help = `
|
help = `
|
||||||
Usage: consul config delete [options] -kind <config kind> -name <config name>
|
Usage: consul config delete [options] ([-kind <config kind> -name <config name>] | [-f FILENAME])
|
||||||
|
|
||||||
Deletes the configuration entry specified by the kind and name.
|
Deletes the configuration entry specified by the kind and name.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
$ consul config delete -kind service-defaults -name web
|
$ consul config delete -kind service-defaults -name web
|
||||||
|
$ consul config delete -filename service-defaults-web.hcl
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent"
|
"github.com/hashicorp/consul/agent"
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/hashicorp/consul/sdk/testutil"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -30,11 +31,7 @@ func TestConfigDelete(t *testing.T) {
|
||||||
ui := cli.NewMockUi()
|
ui := cli.NewMockUi()
|
||||||
c := New(ui)
|
c := New(ui)
|
||||||
|
|
||||||
_, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{
|
err := createEntry(client)
|
||||||
Kind: api.ServiceDefaults,
|
|
||||||
Name: "web",
|
|
||||||
Protocol: "tcp",
|
|
||||||
}, nil)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
|
@ -54,6 +51,15 @@ func TestConfigDelete(t *testing.T) {
|
||||||
require.Nil(t, entry)
|
require.Nil(t, entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createEntry(client *api.Client) error {
|
||||||
|
_, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{
|
||||||
|
Kind: api.ServiceDefaults,
|
||||||
|
Name: "web",
|
||||||
|
Protocol: "tcp",
|
||||||
|
}, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigDelete_CAS(t *testing.T) {
|
func TestConfigDelete_CAS(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("too slow for testing.Short")
|
t.Skip("too slow for testing.Short")
|
||||||
|
@ -65,11 +71,7 @@ func TestConfigDelete_CAS(t *testing.T) {
|
||||||
defer a.Shutdown()
|
defer a.Shutdown()
|
||||||
client := a.Client()
|
client := a.Client()
|
||||||
|
|
||||||
_, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{
|
err := createEntry(client)
|
||||||
Kind: api.ServiceDefaults,
|
|
||||||
Name: "web",
|
|
||||||
Protocol: "tcp",
|
|
||||||
}, nil)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil)
|
entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil)
|
||||||
|
@ -111,11 +113,44 @@ func TestConfigDelete_CAS(t *testing.T) {
|
||||||
require.Contains(t, ui.OutputWriter.String(),
|
require.Contains(t, ui.OutputWriter.String(),
|
||||||
"Config entry deleted: service-defaults/web")
|
"Config entry deleted: service-defaults/web")
|
||||||
require.Empty(t, ui.ErrorWriter.String())
|
require.Empty(t, ui.ErrorWriter.String())
|
||||||
|
|
||||||
entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil)
|
entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Nil(t, entry)
|
require.Nil(t, entry)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("delete from file with a valid modify index", func(t *testing.T) {
|
||||||
|
err := createEntry(client)
|
||||||
|
require.NoError(t, err)
|
||||||
|
entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
c := New(ui)
|
||||||
|
f := testutil.TempFile(t, "config-write-svc-web.hcl")
|
||||||
|
_, err = f.WriteString(`
|
||||||
|
Kind = "service-defaults"
|
||||||
|
Name = "web"
|
||||||
|
Protocol = "tcp"
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"-filename=" + f.Name(),
|
||||||
|
"-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) {
|
func TestConfigDelete_InvalidArgs(t *testing.T) {
|
||||||
|
@ -125,8 +160,12 @@ func TestConfigDelete_InvalidArgs(t *testing.T) {
|
||||||
args []string
|
args []string
|
||||||
err string
|
err string
|
||||||
}{
|
}{
|
||||||
"no kind": {
|
"no kind or filename": {
|
||||||
args: []string{},
|
args: []string{},
|
||||||
|
err: "Must specify the -kind or -filename parameter",
|
||||||
|
},
|
||||||
|
"no kind": {
|
||||||
|
args: []string{"-name", "web"},
|
||||||
err: "Must specify the -kind parameter",
|
err: "Must specify the -kind parameter",
|
||||||
},
|
},
|
||||||
"no name": {
|
"no name": {
|
||||||
|
@ -145,6 +184,18 @@ func TestConfigDelete_InvalidArgs(t *testing.T) {
|
||||||
args: []string{"-kind", api.ServiceDefaults, "-name", "web", "-modify-index", "1"},
|
args: []string{"-kind", api.ServiceDefaults, "-name", "web", "-modify-index", "1"},
|
||||||
err: "Cannot specify -modify-index without -cas",
|
err: "Cannot specify -modify-index without -cas",
|
||||||
},
|
},
|
||||||
|
"kind and filename": {
|
||||||
|
args: []string{"-kind", api.ServiceDefaults, "-filename", "config-file.hcl"},
|
||||||
|
err: "filename can't be used with kind or name",
|
||||||
|
},
|
||||||
|
"name and filename": {
|
||||||
|
args: []string{"-name", "db", "-filename", "config-file.hcl"},
|
||||||
|
err: "filename can't be used with kind or name",
|
||||||
|
},
|
||||||
|
"kind, name, and filename": {
|
||||||
|
args: []string{"-kind", api.ServiceDefaults, "-name", "db", "-filename", "config-file.hcl"},
|
||||||
|
err: "filename can't be used with kind or name",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tcase := range cases {
|
for name, tcase := range cases {
|
||||||
|
|
|
@ -66,7 +66,7 @@ func (c *cmd) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := parseConfigEntry(data)
|
entry, err := helpers.ParseConfigEntry(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.UI.Error(fmt.Sprintf("Failed to decode config entry input: %v", err))
|
c.UI.Error(fmt.Sprintf("Failed to decode config entry input: %v", err))
|
||||||
return 1
|
return 1
|
||||||
|
@ -100,16 +100,6 @@ func (c *cmd) Run(args []string) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseConfigEntry(data string) (api.ConfigEntry, error) {
|
|
||||||
// parse the data
|
|
||||||
var raw map[string]interface{}
|
|
||||||
if err := hclDecode(&raw, data); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to decode config entry input: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return newDecodeConfigEntry(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
// There is a 'structs' variation of this in
|
// There is a 'structs' variation of this in
|
||||||
// agent/structs/config_entry.go:DecodeConfigEntry
|
// agent/structs/config_entry.go:DecodeConfigEntry
|
||||||
func newDecodeConfigEntry(raw map[string]interface{}) (api.ConfigEntry, error) {
|
func newDecodeConfigEntry(raw map[string]interface{}) (api.ConfigEntry, error) {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
||||||
package write
|
package helpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
|
@ -6,6 +6,12 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/hashicorp/consul/lib/decode"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadFromFile(path string) (string, error) {
|
func loadFromFile(path string) (string, error) {
|
||||||
|
@ -59,3 +65,74 @@ func LoadDataSourceNoRaw(data string, testStdin io.Reader) (string, error) {
|
||||||
|
|
||||||
return loadFromFile(data)
|
return loadFromFile(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseConfigEntry(data string) (api.ConfigEntry, error) {
|
||||||
|
// parse the data
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err := hclDecode(&raw, data); err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to decode config entry input: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDecodeConfigEntry(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a 'structs' variation of this in
|
||||||
|
// agent/structs/config_entry.go:DecodeConfigEntry
|
||||||
|
func newDecodeConfigEntry(raw map[string]interface{}) (api.ConfigEntry, error) {
|
||||||
|
var entry api.ConfigEntry
|
||||||
|
|
||||||
|
kindVal, ok := raw["Kind"]
|
||||||
|
if !ok {
|
||||||
|
kindVal, ok = raw["kind"]
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Payload does not contain a kind/Kind key at the top level")
|
||||||
|
}
|
||||||
|
|
||||||
|
if kindStr, ok := kindVal.(string); ok {
|
||||||
|
newEntry, err := api.MakeConfigEntry(kindStr, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entry = newEntry
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("Kind value in payload is not a string")
|
||||||
|
}
|
||||||
|
|
||||||
|
var md mapstructure.Metadata
|
||||||
|
decodeConf := &mapstructure.DecoderConfig{
|
||||||
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
|
decode.HookWeakDecodeFromSlice,
|
||||||
|
decode.HookTranslateKeys,
|
||||||
|
mapstructure.StringToTimeDurationHookFunc(),
|
||||||
|
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||||
|
),
|
||||||
|
Metadata: &md,
|
||||||
|
Result: &entry,
|
||||||
|
WeaklyTypedInput: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder, err := mapstructure.NewDecoder(decodeConf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := decoder.Decode(raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range md.Unused {
|
||||||
|
switch k {
|
||||||
|
case "kind", "Kind":
|
||||||
|
// The kind field is used to determine the target, but doesn't need
|
||||||
|
// to exist on the target.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = multierror.Append(err, fmt.Errorf("invalid config key %q", k))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -56,6 +56,8 @@ Usage: `consul config delete [options]`
|
||||||
`proxy-defaults` config entry must be `global`, and the name of the `mesh`
|
`proxy-defaults` config entry must be `global`, and the name of the `mesh`
|
||||||
config entry must be `mesh`.
|
config entry must be `mesh`.
|
||||||
|
|
||||||
|
- `-filename` - Specifies the file describing the config entry to delete.
|
||||||
|
|
||||||
- `-cas` - Perform a Check-And-Set operation. Specifying this value also
|
- `-cas` - Perform a Check-And-Set operation. Specifying this value also
|
||||||
requires the -modify-index flag to be set. The default value is false.
|
requires the -modify-index flag to be set. The default value is false.
|
||||||
|
|
||||||
|
@ -67,3 +69,5 @@ Usage: `consul config delete [options]`
|
||||||
$ consul config delete -kind service-defaults -name web
|
$ consul config delete -kind service-defaults -name web
|
||||||
|
|
||||||
$ consul config delete -kind service-defaults -name web -cas -modify-index 26
|
$ consul config delete -kind service-defaults -name web -cas -modify-index 26
|
||||||
|
|
||||||
|
$ consul config delete -filename service-defaults-web.hcl
|
||||||
|
|
Loading…
Reference in New Issue