Skip to content

Commit

Permalink
feat(release): newrelic#45 add support for command chaining
Browse files Browse the repository at this point in the history
This PR adds utility methods and logic to read stdin
for details needed in commands, allowing each command
ouput to be piped to the next command

newrelic#45

refactor(release): WIP clean up logic in init function

This commit refactors the init logic to be cleaner, handle
more flags, and prepare for bulk actions

feat(release): WIP collect values from multiple results and add tests

This commit adds more testing and allows reading values from multiple
results

refactor(release): WIP create pipe module

This commit pulls the pipe input logic out of the utils module
and into a new pipe module. It also abstracts the PipeInput map
from other modules.

feat(release): make newrelic entity tags get cmd take stdin

This commit is the last step I can take towards making the
entity tags get command return bulk data based on stdin. Work
has to be done on newrelic-client-go in order to support bulk
guid queries

https://github.com/newrelic/newrelic-client-go/blob/13ed6ee7fa172cce9218cf30e98eab6e1805541d/pkg/entities/tags.go#L124

fix(release): make Get and GetInput always return same values

This commit checks for the existence of the pipeInput internal
state, so even if GetInput is called more than once, nothing is
overwritten and Get returns the same values. More testing and
test refactors are also included.
  • Loading branch information
nicolasjhampton committed Sep 18, 2020
1 parent f03c256 commit 421d868
Show file tree
Hide file tree
Showing 5 changed files with 624 additions and 6 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.6.1
github.com/tidwall/gjson v1.6.1
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0
gopkg.in/yaml.v2 v2.3.0
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,12 @@ github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2 h1:Xr9gkxfOP0K
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
github.com/tetafro/godot v0.4.8 h1:h61+hQraWhdI6WYqMwAwZYCE5yxL6a9/Orw4REbabSU=
github.com/tetafro/godot v0.4.8/go.mod h1:/7NLHhv08H1+8DNj0MElpAACw1ajsCuf3TKNQxA5S+0=
github.com/tidwall/gjson v1.6.1 h1:LRbvNuNuvAiISWg6gxLEFuCe72UKy5hDqhxW/8183ws=
github.com/tidwall/gjson v1.6.1/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e h1:RumXZ56IrCj4CL+g1b9OL/oH0QnsF976bC8xQFYUD5Q=
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
Expand Down
24 changes: 18 additions & 6 deletions internal/entities/command_tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/newrelic/newrelic-cli/internal/client"
"github.com/newrelic/newrelic-cli/internal/output"
"github.com/newrelic/newrelic-cli/internal/pipe"
"github.com/newrelic/newrelic-cli/internal/utils"
"github.com/newrelic/newrelic-client-go/newrelic"
"github.com/newrelic/newrelic-client-go/pkg/entities"
Expand Down Expand Up @@ -40,10 +41,16 @@ The get command returns JSON output of the tags for the requested entity.
Example: "newrelic entity tags get --guid <entityGUID>",
Run: func(cmd *cobra.Command, args []string) {
client.WithClient(func(nrClient *newrelic.NewRelic) {
tags, err := nrClient.Entities.ListTags(entityGUID)
utils.LogIfFatal(err)

utils.LogIfError(output.Print(tags))
// Temporary until bulk actions can be build into newrelic-client-go
if value, ok := pipe.Get("guid"); ok {
tags, err := nrClient.Entities.ListTags(value[0])
utils.LogIfFatal(err)
utils.LogIfError(output.Print(tags))
} else {
tags, err := nrClient.Entities.ListTags(entityGUID)
utils.LogIfFatal(err)
utils.LogIfError(output.Print(tags))
}
})
},
}
Expand Down Expand Up @@ -200,8 +207,13 @@ func init() {
Command.AddCommand(cmdTags)

cmdTags.AddCommand(cmdTagsGet)
cmdTagsGet.Flags().StringVarP(&entityGUID, "guid", "g", "", "the entity GUID to retrieve tags for")
utils.LogIfError(cmdTagsGet.MarkFlagRequired("guid"))

pipe.GetInput([]string{"guid"})

if !pipe.Exists("guid") {
cmdTagsGet.Flags().StringVarP(&entityGUID, "guid", "g", "", "the entity GUID to retrieve tags for")
utils.LogIfError(cmdTagsGet.MarkFlagRequired("guid"))
}

cmdTags.AddCommand(cmdTagsDelete)
cmdTagsDelete.Flags().StringVarP(&entityGUID, "guid", "g", "", "the entity GUID to delete tags on")
Expand Down
136 changes: 136 additions & 0 deletions internal/pipe/pipe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package pipe

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"strings"

"github.com/tidwall/gjson"

"github.com/newrelic/newrelic-cli/internal/utils"
)

type pipeReader interface {
ReadPipe() (string, error)
}

type stdinPipeReader struct {
input io.Reader
}

func (spr stdinPipeReader) ReadPipe() (string, error) {
text := ""
var err error = nil
scanner := bufio.NewScanner(spr.input)

for scanner.Scan() {
text = fmt.Sprintf("%s%s ", text, strings.TrimSpace(scanner.Text()))
}

if scanErr := scanner.Err(); scanErr != nil {
err = scanErr
}

return strings.TrimSpace(text), err
}

func jsonToFilteredMap(r string, selectors []string) ([]map[string]string, error) {
text := r

if !gjson.Valid(text) {
return nil, errors.New("invalid JSON received by stdin")
}

// always start with an array of values
if strings.HasPrefix(text, "{") {
text = fmt.Sprintf("[ %s ]", text)
}

// returns []gjson.Result
jsonArray := gjson.Parse(text).Array()

resultsArray := make([]map[string]string, len(jsonArray))

for index, resultObj := range jsonArray {
resultMap := make(map[string]string)
for _, selector := range selectors {
if value := resultObj.Get(selector); value.Exists() {
// Convert every value to a string
resultMap[selector] = value.String()
}
}
resultsArray[index] = resultMap
}

return resultsArray, nil
}

func readStdin(pipe pipeReader, selectorList []string) ([]map[string]string, error) {
jsonString, pipeErr := pipe.ReadPipe()
if pipeErr != nil {
return nil, pipeErr
}

filteredMap, mapErr := jsonToFilteredMap(jsonString, selectorList)
if mapErr != nil {
return nil, mapErr
}

return filteredMap, nil
}

func pipeInputExists() bool {
fi, err := os.Stdin.Stat()
if err != nil {
return false
}
return (fi.Mode() & os.ModeCharDevice) == 0
}

func getPipeInputInnerFunc(pipe pipeReader, pipeInputExists bool, acceptedPipeInput []string) map[string][]string {
if pipeInputExists {
pipeInputMap := map[string][]string{}
inputArray, err := readStdin(pipe, acceptedPipeInput)
if err != nil {
utils.LogIfError(err)
return map[string][]string{}
}
for _, key := range acceptedPipeInput {
var collectedItemsForKey []string
for _, value := range inputArray {
collectedItemsForKey = append(collectedItemsForKey, value[key])
}
pipeInputMap[key] = collectedItemsForKey
}
return pipeInputMap
}
return map[string][]string{}
}

func getPipeInputFactory(pipe pipeReader, predicate func() bool) func([]string) {
return func(acceptedPipeInput []string) {
if pipeInput == nil {
pipeInput = getPipeInputInnerFunc(pipe, predicate(), acceptedPipeInput)
}
}
}

var pipeInput map[string][]string

var GetInput = getPipeInputFactory(stdinPipeReader{input: os.Stdin}, pipeInputExists)

func Get(inputKey string) ([]string, bool) {
if pipeInput == nil {
return nil, false
}
value, ok := pipeInput[inputKey]
return value, ok
}

func Exists(inputKey string) bool {
_, ok := Get(inputKey)
return ok
}
Loading

0 comments on commit 421d868

Please sign in to comment.