-
Notifications
You must be signed in to change notification settings - Fork 12
/
append.go
194 lines (168 loc) · 4.94 KB
/
append.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package cli
import (
"fmt"
"strconv"
"strings"
"github.com/fishi0x01/vsh/client"
"github.com/fishi0x01/vsh/log"
"github.com/hashicorp/vault/api"
)
// AppendMode defines behaviour in case existing secrets are in conflict
type AppendMode string
const (
// ModeSkip do not append secret on conflict
ModeSkip AppendMode = "skip"
// ModeOverwrite overwrite existing secret on conflict
ModeOverwrite AppendMode = "overwrite"
// ModeRename keep existing secret and create a new key on conflict
ModeRename AppendMode = "rename"
// ModeInvalid denotes invalid mode
ModeInvalid AppendMode = "invalid"
)
// AppendCommand container for all 'append' parameters
type AppendCommand struct {
name string
args *AppendCommandArgs
client *client.Client
Mode AppendMode
}
// AppendCommandArgs provides a struct for go-arg parsing
type AppendCommandArgs struct {
Source string `arg:"positional,required"`
Target string `arg:"positional,required"`
Force bool `arg:"-f,--force" help:"Overwrite key if exists"`
Skip bool `arg:"-s,--skip" help:"Skip key if exists (default)"`
Rename bool `arg:"-r,--rename" help:"Rename key if exists"`
}
// Description provides detail on what the command does
func (AppendCommandArgs) Description() string {
return "appends the contents of one secret to another"
}
// NewAppendCommand creates a new AppendCommand parameter container
func NewAppendCommand(c *client.Client) *AppendCommand {
return &AppendCommand{
name: "append",
client: c,
args: &AppendCommandArgs{},
Mode: ModeSkip,
}
}
// GetName returns the AppendCommand's name identifier
func (cmd *AppendCommand) GetName() string {
return cmd.name
}
// GetArgs provides the struct holding arguments for the command
func (cmd *AppendCommand) GetArgs() interface{} {
return cmd.args
}
// IsSane returns true if command is sane
func (cmd *AppendCommand) IsSane() bool {
return cmd.args.Source != "" && cmd.args.Target != "" && cmd.Mode != ModeInvalid
}
// PrintUsage print command usage
func (cmd *AppendCommand) PrintUsage() {
fmt.Println(Help(cmd))
}
// Parse parses the arguments into the Command and Args structs
func (cmd *AppendCommand) Parse(args []string) error {
_, err := parseCommandArgs(args, cmd)
if err != nil {
return err
}
if cmd.args.Skip == true {
cmd.Mode = ModeSkip
} else if cmd.args.Force == true {
cmd.Mode = ModeOverwrite
} else if cmd.args.Rename == true {
cmd.Mode = ModeRename
}
return nil
}
// Run executes 'append' with given AppendCommand's parameters
func (cmd *AppendCommand) Run() int {
newSrcPwd := cmdPath(cmd.client.Pwd, cmd.args.Source)
newTargetPwd := cmdPath(cmd.client.Pwd, cmd.args.Target)
src := cmd.client.GetType(newSrcPwd)
if src != client.LEAF {
log.UserError("Not a valid path for operation: %s", newSrcPwd)
return 1
}
if err := cmd.mergeSecrets(newSrcPwd, newTargetPwd); err != nil {
log.AppError("Append failed: " + err.Error())
return 1
}
return 0
}
func (cmd *AppendCommand) createDummySecret(target string) error {
targetSecret, err := cmd.client.Read(target)
if targetSecret != nil && err == nil {
return nil
}
dummy := make(map[string]interface{})
dummy["placeholder"] = struct{}{}
dummySecret := client.NewSecret(&api.Secret{Data: dummy})
if targetSecret == nil {
if err = cmd.client.Write(target, dummySecret); err != nil {
return err
}
}
return nil
}
func (cmd *AppendCommand) mergeSecrets(source string, target string) error {
sourceSecret, err := cmd.client.Read(source)
if err != nil {
return err
}
cmd.createDummySecret(target)
targetSecret, err := cmd.client.Read(target)
if err != nil {
return err
}
onConflict := cmd.Mode
merged := targetSecret.GetData()
skippedKeys := make([]string, 0)
for k, v := range sourceSecret.GetData() {
skipped := addKey(merged, onConflict, k, v)
skippedKeys = append(skippedKeys, skipped...)
}
// write
resultSecret := client.NewSecret(&api.Secret{Data: merged})
if err := cmd.client.Write(target, resultSecret); err != nil {
fmt.Println(err)
return err
}
log.UserDebug("Appended values from %s to %s", source, target)
if len(skippedKeys) > 0 {
log.UserDebug("Handled conflicting keys according to the '%s' strategy. Keys: %s", onConflict, strings.Join(skippedKeys, ", "))
}
return nil
}
func addKey(merged map[string]interface{}, onConflict AppendMode, key string, value interface{}) []string {
skippedKeys := make([]string, 0)
// if this key is already present in the destination
if _, ok := merged[key]; ok {
switch onConflict {
case ModeOverwrite:
merged[key] = value
case ModeSkip, ModeInvalid:
skippedKeys = append(skippedKeys, key)
case ModeRename:
key2 := getNextFreeKey(merged, key)
merged[key2] = value
}
} else {
merged[key] = value
}
return skippedKeys
}
func getNextFreeKey(merged map[string]interface{}, key string) string {
idx := 0
ok := true // assume key exists
candidate := key
for ok {
idx++
candidate = key + "_" + strconv.Itoa(idx)
_, ok = merged[candidate]
}
return candidate
}