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  }