Skip to content

Commit 583d9fe

Browse files
authored
Add --insert command to es CLI (#68)
1 parent 688d792 commit 583d9fe

File tree

5 files changed

+170
-34
lines changed

5 files changed

+170
-34
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,29 @@ If your machine is already running MongoDB, be sure to stop it (or modify the po
7272

7373
Then run `go test ./...` from the repo root directory.
7474

75+
## Basic CLI Usage
76+
```shell
77+
> es --insert host hostname=app-001 env=production region=us-west-2
78+
OK, inserted host 67ae6b15e93e456b6ae2d7e4
79+
> es --insert host hostname=app-002 env=production region=us-west-2
80+
OK, inserted host 67ae6b19e93e456b6ae2d7e6
81+
> es --insert host hostname=app-003 env=production region=us-west-2
82+
OK, inserted host 67ae6b1ee93e456b6ae2d7e8
83+
> es --insert host hostname=app-004 env=production region=us-east-1
84+
OK, inserted host 67ae6b25e93e456b6ae2d7ea
85+
> es host region=us-west-2
86+
_id:67ae6b15e93e456b6ae2d7e4,_rev:0,_type:host,env:production,hostname:app-001,region:us-west-2
87+
_id:67ae6b19e93e456b6ae2d7e6,_rev:0,_type:host,env:production,hostname:app-002,region:us-west-2
88+
_id:67ae6b1ee93e456b6ae2d7e8,_rev:0,_type:host,env:production,hostname:app-003,region:us-west-2
89+
> es host region=us-east-1
90+
_id:67ae6b25e93e456b6ae2d7ea,_rev:0,_type:host,env:production,hostname:app-004,region:us-east-1
91+
> es --update host 67ae6b1ee93e456b6ae2d7e8 region=us-east-1
92+
OK, updated host 67ae6b1ee93e456b6ae2d7e8
93+
> es host region=us-west-2
94+
_id:67ae6b15e93e456b6ae2d7e4,_rev:0,_type:host,env:production,hostname:app-001,region:us-west-2
95+
_id:67ae6b19e93e456b6ae2d7e6,_rev:0,_type:host,env:production,hostname:app-002,region:us-west-2
96+
> es host region=us-east-1
97+
_id:67ae6b1ee93e456b6ae2d7e8,_rev:1,_type:host,env:production,hostname:app-003,region:us-east-1
98+
_id:67ae6b25e93e456b6ae2d7ea,_rev:0,_type:host,env:production,hostname:app-004,region:us-east-1
99+
>
100+
```

es/app/app.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type Context struct {
2727
ReturnLabels []string // query
2828
Query string // query
2929
EntityId string // --update and --delete
30-
Patches []string // --update
30+
Patches []string // --update and --insert
3131
}
3232

3333
type EntityClientFactory interface {
@@ -42,6 +42,7 @@ type Hooks struct {
4242
AfterParseOptions func(*config.Options)
4343
BeforeQuery func(*Context) error
4444
AfterQuery func(Context, []etre.Entity, error)
45+
BeforeInsert func(cxt *Context) error
4546
BeforeDelete func(ctx *Context) error
4647
BeforeUpdate func(cxt *Context) error
4748
WriteResult func(Context, etre.WriteResult, error)

es/config/config.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type Options struct {
4343
Help bool
4444
JSON bool `arg:"env:ES_JSON" yaml:"json"`
4545
IFS string `arg:"env" yaml:"ifs"`
46+
Insert bool
4647
Labels bool `arg:"env:ES_LABELS" yaml:"labels"`
4748
Old bool `arg:"env:ES_OLD" yaml:"old"`
4849
QueryTimeout string `arg:"--query-timeout,env:ES_QUERY_TIMEOUT" yaml:"query_timeout"`
@@ -64,9 +65,8 @@ type Options struct {
6465
// labels, and query predicates. The caller is expected to copy and use the embedded
6566
// structs separately, like:
6667
//
67-
// var o config.Options = cmdLine.Options
68-
// for i, arg := range cmdline.Args {
69-
//
68+
// var o config.Options = cmdLine.Options
69+
// for i, arg := range cmdline.Args {
7070
type CommandLine struct {
7171
Options
7272
Args []string `arg:"positional"`
@@ -100,6 +100,7 @@ func ParseCommandLine(def Options) CommandLine {
100100
func Help() {
101101
fmt.Printf("Usage:\n"+
102102
" Query: es [options] entity[.label,...] query\n"+
103+
" Insert Entity: es [options] --insert entity patches\n"+
103104
" Update Entity: es [options] --update entity id patches\n"+
104105
" Delete Entity: es [options] --delete entity id\n\n"+
105106
" Delete Label: es [options] --delete-label entity id label\n"+

es/es.go

Lines changed: 88 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,12 @@ func Run(ctx app.Context) {
8181
if cmdLine.Options.Update {
8282
writeOptions++
8383
}
84+
if cmdLine.Options.Insert {
85+
writeOptions++
86+
}
8487
if writeOptions > 1 {
8588
config.Help()
86-
fmt.Fprintf(os.Stderr, "--update, --delete, and --delete-label are mutually exclusive\n")
89+
fmt.Fprintf(os.Stderr, "--insert, --update, --delete, and --delete-label are mutually exclusive\n")
8790
os.Exit(1)
8891
}
8992

@@ -117,6 +120,12 @@ func Run(ctx app.Context) {
117120
fmt.Fprintf(os.Stderr, "Not enough arguments for --update: entity, id, and patches are required\n")
118121
os.Exit(1)
119122
}
123+
} else if cmdLine.Options.Insert { // --insert
124+
if len(cmdLine.Args) < 2 {
125+
config.Help()
126+
fmt.Fprintf(os.Stderr, "Not enough arguments for --insert: entity and patches are required\n")
127+
os.Exit(1)
128+
}
120129
} else if cmdLine.Options.Watch { // --watch
121130
} else { // query
122131
if len(cmdLine.Args) < 2 {
@@ -265,6 +274,39 @@ func Run(ctx app.Context) {
265274
etre.Debug("trace: %s", trace)
266275
ec = ec.WithTrace(trace)
267276

277+
// //////////////////////////////////////////////////////////////////////
278+
// Insert and exit, if --insert
279+
// //////////////////////////////////////////////////////////////////////
280+
281+
if o.Insert {
282+
// cmdLine.Args validated above
283+
ctx.Patches = cmdLine.Args[1:]
284+
285+
if ctx.Hooks.BeforeInsert != nil {
286+
etre.Debug("calling hook BeforeInsert")
287+
err = ctx.Hooks.BeforeInsert(&ctx)
288+
if err != nil {
289+
printAndExit(fmt.Errorf("BeforeInsert hook failed: %s", err), ctx)
290+
}
291+
}
292+
293+
patch, err := parsePatches(ctx)
294+
if err != nil {
295+
printAndExit(err, ctx)
296+
}
297+
298+
wr, err := ec.Insert([]etre.Entity{patch})
299+
_, err = writeResult(ctx, set, wr, err, "insert")
300+
if err != nil {
301+
printAndExit(err, ctx)
302+
}
303+
if len(wr.Writes) != 1 {
304+
printAndExit(fmt.Errorf("Etre reports no error but reported %d inserts, expected 1 insert", len(wr.Writes)), ctx)
305+
}
306+
fmt.Printf("OK, inserted %s %s\n", ctx.EntityType, wr.Writes[0].EntityId)
307+
return
308+
}
309+
268310
// //////////////////////////////////////////////////////////////////////
269311
// Update and exit, if --update
270312
// //////////////////////////////////////////////////////////////////////
@@ -276,37 +318,16 @@ func Run(ctx app.Context) {
276318

277319
if ctx.Hooks.BeforeUpdate != nil {
278320
etre.Debug("calling hook BeforeUpdate")
279-
ctx.Hooks.BeforeUpdate(&ctx)
321+
err = ctx.Hooks.BeforeUpdate(&ctx)
322+
if err != nil {
323+
printAndExit(fmt.Errorf("BeforeUpdate hook failed: %s", err), ctx)
324+
}
280325
}
281326

282-
patch := etre.Entity{}
283-
for i, kv := range ctx.Patches {
284-
p := strings.SplitN(kv, "=", 2)
285-
etre.Debug("patch %d: '%s': %#v", i, kv, p)
286-
if len(p) > 0 {
287-
if ctx.Options.Strict {
288-
if strings.IndexAny(p[0], " \t") != -1 {
289-
printAndExit(fmt.Errorf("Invalid patch: %s: label has whitespace", kv), ctx)
290-
}
291-
} else {
292-
p[0] = strings.TrimSpace(p[0])
293-
}
294-
if p[0] == "" {
295-
printAndExit(fmt.Errorf("Invalid patch: %s: empty label", kv), ctx)
296-
}
297-
}
298-
switch len(p) {
299-
case 0:
300-
printAndExit(fmt.Errorf("Invalid patch: %s: split on = yielded 0 parts, expected 1 or 2", kv), ctx)
301-
case 1:
302-
patch[p[0]] = nil
303-
case 2:
304-
patch[p[0]] = p[1]
305-
default:
306-
printAndExit(fmt.Errorf("Invalid patch: %s: split on = yielded %d parts, expected 2", kv, len(p)), ctx)
307-
}
327+
patch, err := parsePatches(ctx)
328+
if err != nil {
329+
printAndExit(err, ctx)
308330
}
309-
etre.Debug("patch: %#v", patch)
310331

311332
wr, err := ec.UpdateOne(ctx.EntityId, patch)
312333
found, err := writeResult(ctx, set, wr, err, "update")
@@ -331,7 +352,10 @@ func Run(ctx app.Context) {
331352

332353
if ctx.Hooks.BeforeDelete != nil {
333354
etre.Debug("calling hook BeforeDelete")
334-
ctx.Hooks.BeforeDelete(&ctx)
355+
err = ctx.Hooks.BeforeDelete(&ctx)
356+
if err != nil {
357+
printAndExit(fmt.Errorf("BeforeDelete hook failed: %s", err), ctx)
358+
}
335359
}
336360

337361
wr, err := ec.DeleteOne(ctx.EntityId)
@@ -527,3 +551,37 @@ func printAndExit(err error, ctx app.Context) {
527551
}
528552
os.Exit(1)
529553
}
554+
555+
func parsePatches(ctx app.Context) (etre.Entity, error) {
556+
patch := etre.Entity{}
557+
558+
for i, kv := range ctx.Patches {
559+
p := strings.SplitN(kv, "=", 2)
560+
etre.Debug("patch %d: '%s': %#v", i, kv, p)
561+
if len(p) > 0 {
562+
if ctx.Options.Strict {
563+
if strings.IndexAny(p[0], " \t") != -1 {
564+
return patch, fmt.Errorf("Invalid patch: %s: label has whitespace", kv)
565+
}
566+
} else {
567+
p[0] = strings.TrimSpace(p[0])
568+
}
569+
if p[0] == "" {
570+
return patch, fmt.Errorf("Invalid patch: %s: empty label", kv)
571+
}
572+
}
573+
switch len(p) {
574+
case 0:
575+
return patch, fmt.Errorf("Invalid patch: %s: split on = yielded 0 parts, expected 1 or 2", kv)
576+
case 1:
577+
patch[p[0]] = nil
578+
case 2:
579+
patch[p[0]] = strings.TrimSpace(p[1])
580+
default:
581+
return patch, fmt.Errorf("Invalid patch: %s: split on = yielded %d parts, expected 2", kv, len(p))
582+
}
583+
}
584+
etre.Debug("patch: %#v", patch)
585+
586+
return patch, nil
587+
}

es/es_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package es
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/square/etre/es/app"
10+
"github.com/square/etre/es/config"
11+
)
12+
13+
func TestParsePatches(t *testing.T) {
14+
testCases := []struct {
15+
name string
16+
raw []string
17+
parsed map[string]string
18+
err bool
19+
strict bool
20+
}{
21+
{name: "happy path", raw: []string{"a=1", "b=2", "c=3"}, parsed: map[string]string{"a": "1", "b": "2", "c": "3"}},
22+
{name: "whitespace around value", raw: []string{"a= 1 ", "b= 2 ", "c=\t3\t"}, parsed: map[string]string{"a": "1", "b": "2", "c": "3"}},
23+
{name: "whitespace around label", raw: []string{" a=1", " b =2", "\tc\t=3"}, parsed: map[string]string{"a": "1", "b": "2", "c": "3"}},
24+
{name: "value contains whitespace", raw: []string{"a= 1 2 "}, parsed: map[string]string{"a": "1 2"}},
25+
{name: "value contains equals", raw: []string{"a=1=2"}, parsed: map[string]string{"a": "1=2"}},
26+
{name: "empty value", raw: []string{"a="}, parsed: map[string]string{"a": ""}},
27+
{name: "missing label", raw: []string{"=1"}, err: true},
28+
{name: "missing label and value", raw: []string{"="}, err: true},
29+
{name: "empty string", raw: []string{""}, err: true},
30+
{name: "strict label whitespace", raw: []string{" a =1"}, err: true, strict: true},
31+
}
32+
33+
for _, tc := range testCases {
34+
t.Run(tc.name, func(t *testing.T) {
35+
ctx := app.Context{
36+
Patches: tc.raw,
37+
Options: config.Options{Strict: tc.strict},
38+
}
39+
parsed, err := parsePatches(ctx)
40+
if tc.err {
41+
assert.Error(t, err, tc.name)
42+
} else {
43+
require.NoError(t, err, tc.name)
44+
for k, v := range tc.parsed {
45+
assert.Equal(t, v, parsed[k], "bad value for label %s", k)
46+
}
47+
}
48+
})
49+
}
50+
}

0 commit comments

Comments
 (0)