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"
|
||||
|
||||
"github.com/hashicorp/consul/command/flags"
|
||||
"github.com/hashicorp/consul/command/helpers"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
|
@ -25,10 +26,13 @@ type cmd struct {
|
|||
name string
|
||||
cas bool
|
||||
modifyIndex uint64
|
||||
filename string
|
||||
}
|
||||
|
||||
func (c *cmd) init() {
|
||||
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.name, "name", "", "The name of configuration to delete.")
|
||||
c.flags.BoolVar(&c.cas, "cas", false,
|
||||
|
@ -55,6 +59,25 @@ func (c *cmd) Run(args []string) int {
|
|||
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()
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
|
||||
|
@ -64,33 +87,58 @@ func (c *cmd) Run(args []string) int {
|
|||
|
||||
var deleted bool
|
||||
if c.cas {
|
||||
deleted, _, err = entries.DeleteCAS(c.kind, c.name, c.modifyIndex, nil)
|
||||
deleted, _, err = entries.DeleteCAS(kind, name, c.modifyIndex, nil)
|
||||
} else {
|
||||
_, err = entries.Delete(c.kind, c.name, nil)
|
||||
_, err = entries.Delete(kind, name, nil)
|
||||
deleted = 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (c *cmd) validateArgs() error {
|
||||
if c.kind == "" {
|
||||
return errors.New("Must specify the -kind parameter")
|
||||
count := 0
|
||||
if c.filename != "" {
|
||||
count++
|
||||
}
|
||||
|
||||
if c.name == "" {
|
||||
return errors.New("Must specify the -name parameter")
|
||||
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 == "" {
|
||||
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 {
|
||||
|
@ -115,12 +163,13 @@ func (c *cmd) Help() string {
|
|||
const (
|
||||
synopsis = "Delete a centralized config entry"
|
||||
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.
|
||||
|
||||
Example:
|
||||
|
||||
$ 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/api"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -30,11 +31,7 @@ func TestConfigDelete(t *testing.T) {
|
|||
ui := cli.NewMockUi()
|
||||
c := New(ui)
|
||||
|
||||
_, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{
|
||||
Kind: api.ServiceDefaults,
|
||||
Name: "web",
|
||||
Protocol: "tcp",
|
||||
}, nil)
|
||||
err := createEntry(client)
|
||||
require.NoError(t, err)
|
||||
|
||||
args := []string{
|
||||
|
@ -54,6 +51,15 @@ func TestConfigDelete(t *testing.T) {
|
|||
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) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
|
@ -65,11 +71,7 @@ func TestConfigDelete_CAS(t *testing.T) {
|
|||
defer a.Shutdown()
|
||||
client := a.Client()
|
||||
|
||||
_, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{
|
||||
Kind: api.ServiceDefaults,
|
||||
Name: "web",
|
||||
Protocol: "tcp",
|
||||
}, nil)
|
||||
err := createEntry(client)
|
||||
require.NoError(t, err)
|
||||
|
||||
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(),
|
||||
"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)
|
||||
})
|
||||
|
||||
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) {
|
||||
|
@ -125,8 +160,12 @@ func TestConfigDelete_InvalidArgs(t *testing.T) {
|
|||
args []string
|
||||
err string
|
||||
}{
|
||||
"no kind": {
|
||||
"no kind or filename": {
|
||||
args: []string{},
|
||||
err: "Must specify the -kind or -filename parameter",
|
||||
},
|
||||
"no kind": {
|
||||
args: []string{"-name", "web"},
|
||||
err: "Must specify the -kind parameter",
|
||||
},
|
||||
"no name": {
|
||||
|
@ -145,6 +184,18 @@ func TestConfigDelete_InvalidArgs(t *testing.T) {
|
|||
args: []string{"-kind", api.ServiceDefaults, "-name", "web", "-modify-index", "1"},
|
||||
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 {
|
||||
|
|
|
@ -66,7 +66,7 @@ func (c *cmd) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
entry, err := parseConfigEntry(data)
|
||||
entry, err := helpers.ParseConfigEntry(data)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Failed to decode config entry input: %v", err))
|
||||
return 1
|
||||
|
@ -100,16 +100,6 @@ func (c *cmd) Run(args []string) int {
|
|||
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
|
||||
// agent/structs/config_entry.go:DecodeConfigEntry
|
||||
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 (
|
||||
"encoding/json"
|
|
@ -6,6 +6,12 @@ import (
|
|||
"io"
|
||||
"io/ioutil"
|
||||
"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) {
|
||||
|
@ -59,3 +65,74 @@ func LoadDataSourceNoRaw(data string, testStdin io.Reader) (string, error) {
|
|||
|
||||
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`
|
||||
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
|
||||
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 -cas -modify-index 26
|
||||
|
||||
$ consul config delete -filename service-defaults-web.hcl
|
||||
|
|
Loading…
Reference in New Issue