github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/appbuilder/kv/kv.go (about) 1 // Copyright (c) 2022, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package kv 6 7 import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "strings" 13 "time" 14 15 "github.com/choria-io/appbuilder/builder" 16 "github.com/choria-io/fisk" 17 "github.com/choria-io/go-choria/choria" 18 "github.com/choria-io/go-choria/config" 19 "github.com/choria-io/go-choria/internal/util" 20 "github.com/nats-io/nats.go" 21 "github.com/sirupsen/logrus" 22 ) 23 24 type Command struct { 25 Action string `json:"action"` 26 Bucket string `json:"bucket"` 27 Key string `json:"key"` 28 Value string `json:"value"` 29 RenderJSON bool `json:"json"` 30 Transform *builder.Transform `json:"transform"` 31 32 builder.GenericCommand 33 builder.GenericSubCommands 34 } 35 36 type KV struct { 37 b *builder.AppBuilder 38 arguments map[string]any 39 flags map[string]any 40 cmd *fisk.CmdClause 41 def *Command 42 cfg any 43 log builder.Logger 44 ctx context.Context 45 } 46 47 func NewKVCommand(b *builder.AppBuilder, j json.RawMessage, log builder.Logger) (builder.Command, error) { 48 kv := &KV{ 49 def: &Command{}, 50 cfg: b.Configuration(), 51 ctx: b.Context(), 52 b: b, 53 log: log, 54 arguments: map[string]any{}, 55 flags: map[string]any{}, 56 } 57 58 err := json.Unmarshal(j, kv.def) 59 if err != nil { 60 return nil, err 61 } 62 63 return kv, nil 64 } 65 66 func Register() error { 67 return builder.RegisterCommand("kv", NewKVCommand) 68 } 69 70 func MustRegister() { 71 builder.MustRegisterCommand("kv", NewKVCommand) 72 } 73 74 func (r *KV) Validate(log builder.Logger) error { 75 if r.def.Type != "kv" { 76 return fmt.Errorf("not a kv command") 77 } 78 79 var errs []string 80 81 err := r.def.GenericCommand.Validate(log) 82 if err != nil { 83 errs = append(errs, err.Error()) 84 } 85 86 if r.def.Transform != nil { 87 err := r.def.Transform.Validate(log) 88 if err != nil { 89 errs = append(errs, err.Error()) 90 } 91 } 92 93 if r.def.Bucket == "" { 94 errs = append(errs, "bucket is required") 95 } 96 97 if r.def.Key == "" { 98 errs = append(errs, "key is required") 99 } 100 101 act := r.def.Action 102 if act == "put" && r.def.Value == "" { 103 errs = append(errs, "value is required for put operations") 104 } 105 106 if !(act == "put" || act == "get" || act == "history" || act == "del") { 107 errs = append(errs, fmt.Sprintf("invalid action %q", act)) 108 } 109 110 if len(errs) > 0 { 111 return errors.New(strings.Join(errs, ", ")) 112 } 113 114 return nil 115 } 116 117 func (r *KV) String() string { return fmt.Sprintf("%s (kv)", r.def.Name) } 118 119 func (r *KV) SubCommands() []json.RawMessage { 120 return r.def.Commands 121 } 122 123 func (r *KV) CreateCommand(app builder.KingpinCommand) (*fisk.CmdClause, error) { 124 r.cmd = builder.CreateGenericCommand(app, &r.def.GenericCommand, r.arguments, r.flags, r.b, r.runCommand) 125 126 if r.def.Action == "get" || r.def.Action == "history" && !r.def.RenderJSON { 127 r.cmd.Flag("json", "Renders results in JSON format").BoolVar(&r.def.RenderJSON) 128 } 129 130 return r.cmd, nil 131 } 132 133 func (r *KV) getAction(kv nats.KeyValue) error { 134 key, err := r.key() 135 if err != nil { 136 return err 137 } 138 139 entry, err := kv.Get(key) 140 if err != nil { 141 return err 142 } 143 144 switch { 145 case r.def.Transform != nil: 146 res, err := r.def.Transform.TransformBytes(r.ctx, entry.Value(), r.arguments, r.flags, r.b) 147 if err != nil { 148 return err 149 } 150 fmt.Println(string(res)) 151 152 case r.def.RenderJSON: 153 ej, err := json.MarshalIndent(r.entryMap(entry), "", " ") 154 if err != nil { 155 return err 156 } 157 158 fmt.Println(string(ej)) 159 160 default: 161 fmt.Println(string(entry.Value())) 162 } 163 164 return nil 165 } 166 167 func (r *KV) putAction(kv nats.KeyValue) error { 168 v, err := builder.ParseStateTemplate(r.def.Value, r.arguments, r.flags, r.cfg) 169 if err != nil { 170 return err 171 } 172 173 key, err := r.key() 174 if err != nil { 175 return err 176 } 177 178 rev, err := kv.PutString(key, v) 179 if err != nil { 180 return err 181 } 182 183 fmt.Printf("Wrote revision %d\n", rev) 184 185 return nil 186 } 187 188 func (r *KV) delAction(kv nats.KeyValue) error { 189 key, err := r.key() 190 if err != nil { 191 return err 192 } 193 194 err = kv.Delete(key) 195 if err != nil { 196 return err 197 } 198 fmt.Printf("Deleted key %s\n", key) 199 200 return nil 201 } 202 203 func (r *KV) opStringForOp(kvop nats.KeyValueOp) string { 204 var op string 205 206 switch kvop { 207 case nats.KeyValuePurge: 208 op = "PURGE" 209 case nats.KeyValueDelete: 210 op = "DELETE" 211 case nats.KeyValuePut: 212 op = "PUT" 213 default: 214 op = kvop.String() 215 } 216 217 return op 218 } 219 220 func (r *KV) entryMap(e nats.KeyValueEntry) map[string]any { 221 if e == nil { 222 return nil 223 } 224 225 res := map[string]any{ 226 "operation": r.opStringForOp(e.Operation()), 227 "revision": e.Revision(), 228 "value": util.Base64IfNotPrintable(e.Value()), 229 "created": e.Created().Unix(), 230 } 231 232 return res 233 } 234 235 func (r *KV) historyAction(kv nats.KeyValue) error { 236 key, err := r.key() 237 if err != nil { 238 return err 239 } 240 241 history, err := kv.History(key) 242 if err != nil { 243 return err 244 } 245 246 if r.def.RenderJSON { 247 hist := map[string]map[string][]any{} 248 for _, e := range history { 249 if _, ok := hist[e.Bucket()]; !ok { 250 hist[e.Bucket()] = map[string][]any{} 251 } 252 253 hist[e.Bucket()][e.Key()] = append(hist[e.Bucket()][e.Key()], r.entryMap(e)) 254 } 255 256 j, err := json.MarshalIndent(hist, "", " ") 257 if err != nil { 258 return err 259 } 260 261 fmt.Println(string(j)) 262 return nil 263 } 264 265 table := util.NewUTF8Table("Seq", "Operation", "Time", "Value") 266 for _, e := range history { 267 val := util.Base64IfNotPrintable(e.Value()) 268 if len(val) > 40 { 269 val = fmt.Sprintf("%s...%s", val[0:15], val[len(val)-15:]) 270 } 271 272 table.AddRow(e.Revision(), r.opStringForOp(e.Operation()), e.Created().Format(time.RFC822), val) 273 } 274 275 fmt.Println(table.Render()) 276 277 return nil 278 } 279 280 func (r *KV) bucket() (string, error) { 281 return builder.ParseStateTemplate(r.def.Bucket, r.arguments, r.flags, r.cfg) 282 } 283 284 func (r *KV) key() (string, error) { 285 return builder.ParseStateTemplate(r.def.Key, r.arguments, r.flags, r.cfg) 286 } 287 288 func (r *KV) runCommand(_ *fisk.ParseContext) error { 289 cfg, err := config.NewConfig(choria.UserConfig()) 290 if err != nil { 291 return err 292 } 293 294 logger, ok := any(r.log).(*logrus.Logger) 295 if ok { 296 cfg.CustomLogger = logger 297 } 298 299 fw, err := choria.NewWithConfig(cfg) 300 if err != nil { 301 return err 302 } 303 304 bucket, err := r.bucket() 305 if err != nil { 306 return err 307 } 308 309 kv, err := fw.KV(r.ctx, nil, bucket, false) 310 if err != nil { 311 return err 312 } 313 314 switch r.def.Action { 315 case "get": 316 err = r.getAction(kv) 317 318 case "put": 319 err = r.putAction(kv) 320 321 case "del": 322 err = r.delAction(kv) 323 324 case "history": 325 err = r.historyAction(kv) 326 } 327 328 return err 329 }