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:
cskh 2022-07-11 10:13:40 -04:00 committed by GitHub
parent 18aacf9b55
commit 2d99304762
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 3288 additions and 3098 deletions

4
.changelog/13677.txt Normal file
View File

@ -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
```

View File

@ -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,33 +87,58 @@ 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 {
if c.kind == "" { count := 0
return errors.New("Must specify the -kind parameter") if c.filename != "" {
count++
} }
if c.name == "" { if c.kind != "" {
return errors.New("Must specify the -name parameter") 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 { if c.cas && c.modifyIndex == 0 {
@ -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
` `
) )

View File

@ -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 {

View File

@ -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

View File

@ -1,4 +1,4 @@
package write package helpers
import ( import (
"encoding/json" "encoding/json"

View File

@ -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

View File

@ -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