Skip to content

Commit

Permalink
add an add command
Browse files Browse the repository at this point in the history
this command is basically `vault kv patch` but it's nice to have inside
vsh when in interactive mode. you can simply add a key/value to a path.
this can save you time if you're doing other operations inside vsh.

i was also able to eliminate another duplication when creating new
commands by making fetching the Command object from the Commands struct
dynamic.
  • Loading branch information
Matt Kulka committed Feb 21, 2021
1 parent af2a284 commit 9e4a1ed
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 35 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## master - unreleased

ENHANCEMENTS:

* Add `add` command for single key insertion ([#86](https://github.com/fishi0x01/vsh/pull/87))

BUG FIXES:

* Don't show error on empty line enter in interactive mode ([#85](https://github.com/fishi0x01/vsh/pull/85))
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Download latest static binaries from [release page](https://github.com/fishi0x01

## Supported commands

- [add](doc/commands/add.md) adds a single key and value to a path
- [append](doc/commands/append.md) merges secrets with different strategies (allows recursive operation on paths)
- [cat](doc/commands/cat.md) shows the key/value pairs of a path
- [cd](doc/commands/cd.md) allows interactive navigation through the paths
Expand Down
101 changes: 101 additions & 0 deletions cli/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package cli

import (
"fmt"

"github.com/fishi0x01/vsh/client"
"github.com/fishi0x01/vsh/log"
)

// AddCommand container for all 'append' parameters
type AddCommand struct {
name string
args *AddCommandArgs

client *client.Client
}

// AddCommandArgs provides a struct for go-arg parsing
type AddCommandArgs struct {
Key string `arg:"positional,required"`
Value string `arg:"positional,required"`
Path string `arg:"positional,required"`
Force bool `arg:"-f,--force" help:"Overwrite key if exists"`
}

// Description provides detail on what the command does
func (AddCommandArgs) Description() string {
return "adds a key with value to a path"
}

// NewAddCommand creates a new AddCommand parameter container
func NewAddCommand(c *client.Client) *AddCommand {
return &AddCommand{
name: "add",
client: c,
args: &AddCommandArgs{},
}
}

// GetName returns the AddCommand's name identifier
func (cmd *AddCommand) GetName() string {
return cmd.name
}

// GetArgs provides the struct holding arguments for the command
func (cmd *AddCommand) GetArgs() interface{} {
return cmd.args
}

// IsSane returns true if command is sane
func (cmd *AddCommand) IsSane() bool {
return cmd.args.Key != "" && cmd.args.Value != "" && cmd.args.Path != ""
}

// PrintUsage print command usage
func (cmd *AddCommand) PrintUsage() {
fmt.Println(Help(cmd))
}

// Parse parses the arguments into the Command and Args structs
func (cmd *AddCommand) Parse(args []string) error {
_, err := parseCommandArgs(args, cmd)
if err != nil {
return err
}

return nil
}

// Run executes 'add' with given AddCommand's parameters
func (cmd *AddCommand) Run() int {
path := cmdPath(cmd.client.Pwd, cmd.args.Path)

pathType := cmd.client.GetType(path)
if pathType != client.LEAF {
log.UserError("Not a valid path for operation: %s", path)
return 1
}

err := cmd.addKeyValue(cmd.args.Path, cmd.args.Key, cmd.args.Value)
if err != nil {
log.UserError("Add failed: " + err.Error())
return 1
}

return 0
}

func (cmd *AddCommand) addKeyValue(path string, key string, value string) error {
secret, err := cmd.client.Read(path)
if err != nil {
return fmt.Errorf("Read failed: %s", err)
}
data := secret.GetData()
if _, ok := data[key]; ok && !cmd.args.Force {
return fmt.Errorf("Key already exists at path: %s", path)
}
data[key] = value
secret.SetData(data)
return cmd.client.Write(path, secret)
}
13 changes: 13 additions & 0 deletions cli/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"path/filepath"
"strings"

"github.com/fatih/structs"
"github.com/fishi0x01/vsh/client"
"github.com/fishi0x01/vsh/log"
)
Expand All @@ -20,6 +21,7 @@ type Command interface {

// Commands contains all available commands
type Commands struct {
Add *AddCommand
Append *AppendCommand
Cat *CatCommand
Cd *CdCommand
Expand All @@ -31,9 +33,20 @@ type Commands struct {
Rm *RemoveCommand
}

// Get returns the Command that matches the string
func (cmds *Commands) Get(cmd string) Command {
for _, f := range structs.Fields(cmds) {
if c := f.Value().(Command); cmd == c.GetName() {
return c
}
}
return nil
}

// NewCommands returns a Commands struct with all available commands
func NewCommands(client *client.Client) *Commands {
return &Commands{
Add: NewAddCommand(client),
Append: NewAppendCommand(client),
Cat: NewCatCommand(client),
Cd: NewCdCommand(client),
Expand Down
7 changes: 5 additions & 2 deletions client/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,11 @@ func transformToKV2Secret(secret api.Secret) *api.Secret {
}

func normalizedVaultPath(absolutePath string) string {
// remove trailing '/'
return absolutePath[1:]
// remove leading '/'
if absolutePath[0] == '/' {
return absolutePath[1:]
}
return absolutePath
}

func sliceContains(arr []string, search string) bool {
Expand Down
23 changes: 23 additions & 0 deletions doc/commands/add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# add

```text
add [-f|--force] KEY VALUE PATH
```

Add operation adds or overwrites a single key at path.

By default, it will add the key if it does not already exist. To overwrite the key if it exists, use the `--force` flag.

```bash
> cat secret/path

value = 1
other = thing

> add fizz buzz secret/path
> cat /secret/to

value = 1
fizz = buzz
other = thing
```
44 changes: 11 additions & 33 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,16 @@ func executor(in string) {
os.Exit(0)
default:
cmd, err = getCommand(args[0], commands)
if err == nil {
err = cmd.Parse(args[0])
}
}

if err != nil && cmd != nil {
if err != nil {
log.UserError("%v", err)
cmd.PrintUsage()
if cmd != nil {
cmd.PrintUsage()
}
}

if err != nil && !isInteractive {
Expand All @@ -94,38 +99,11 @@ func executor(in string) {
}

func getCommand(args []string, commands *cli.Commands) (cmd cli.Command, err error) {
switch args[0] {
case commands.Ls.GetName():
err = commands.Ls.Parse(args)
cmd = commands.Ls
case commands.Cd.GetName():
err = commands.Cd.Parse(args)
cmd = commands.Cd
case commands.Mv.GetName():
err = commands.Mv.Parse(args)
cmd = commands.Mv
case commands.Append.GetName():
err = commands.Append.Parse(args)
cmd = commands.Append
case commands.Cp.GetName():
err = commands.Cp.Parse(args)
cmd = commands.Cp
case commands.Rm.GetName():
err = commands.Rm.Parse(args)
cmd = commands.Rm
case commands.Cat.GetName():
err = commands.Cat.Parse(args)
cmd = commands.Cat
case commands.Grep.GetName():
err = commands.Grep.Parse(args)
cmd = commands.Grep
case commands.Replace.GetName():
err = commands.Replace.Parse(args)
cmd = commands.Replace
default:
log.UserError("Not a valid command: %s", args[0])
return nil, fmt.Errorf("not a valid command")
cmd = commands.Get(args[0])
if cmd == nil {
return nil, fmt.Errorf("not a valid command: %s", args[0])
}

return cmd, err
}

Expand Down
37 changes: 37 additions & 0 deletions test/suites/commands/add.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
load ../../util/common
load ../../util/standard-setup
load ../../bin/plugins/bats-support/load
load ../../bin/plugins/bats-assert/load

@test "vault-${VAULT_VERSION} ${KV_BACKEND} 'add'" {
#######################################
echo "==== case: add value to non existing path ===="
run ${APP_BIN} -c "add test value ${KV_BACKEND}/fake/path"
assert_failure

#######################################
echo "==== case: add key to existing path ===="
run ${APP_BIN} -c "add test value ${KV_BACKEND}/src/a/foo"
assert_success

echo "ensure the key was written to destination"
run get_vault_value "test" "${KV_BACKEND}/src/a/foo"
assert_success
assert_output "value"

#######################################
echo "==== case: add existing key to existing path ===="
run ${APP_BIN} -c "add value another ${KV_BACKEND}/src/a/foo"
assert_failure
assert_output --partial "Key already exists at path: ${KV_BACKEND}/src/a/foo"

#######################################
echo "==== case: overwrite existing key to existing path ===="
run ${APP_BIN} -c "add -f value another ${KV_BACKEND}/src/a/foo"
assert_success

echo "ensure the key was written to destination"
run get_vault_value "value" "${KV_BACKEND}/src/a/foo"
assert_success
assert_output "another"
}

0 comments on commit 9e4a1ed

Please sign in to comment.