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.

docs(release): add docs comments to pipe package
  • Loading branch information
nicolasjhampton committed Sep 18, 2020
1 parent f03c256 commit fc41f15
Show file tree
Hide file tree
Showing 5 changed files with 650 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
162 changes: 162 additions & 0 deletions internal/pipe/pipe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Package pipe provides a simple API to read and retrieve values
// from stdin to use in Cobra commands. Public API consists of
// GetInput, which reads stdin, Exists, which checks for value
// existence, and Get for retriving existing values.
package pipe

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

"github.com/tidwall/gjson"

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

// Created Interface and struct to surround io.Reader for easy mocking
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
}

// Standard way to check for stdin in most enviorments (https://stackoverflow.com/questions/22563616/determine-if-stdin-has-data-with-go)
func pipeInputExists() bool {
fi, err := os.Stdin.Stat()
if err != nil {
return false
}
return (fi.Mode() & os.ModeCharDevice) == 0
}

// getPipeInputInnerFunc is the function returned with arguments by
// getPipeInputFactory that becomes GetInput. Private to package, main
// function to be tested.
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{}
}

// getPipeInputFactory is a factory method to create GetInput. Allows for
// extensive testing options using dependency injection. Only ever called
// once on the first import of the package. Private to the package.
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

// GetInput takes a slice of gjson selectors (https://github.com/tidwall/gjson/blob/master/SYNTAX.md)
// as an argument. When ran once at the top the init function, GetInput
// stores those desired json values from stdin. The existence of and values
// of those stdin json keys can then be retrived using the public Exists and
// Get methods, respectively.
var GetInput = getPipeInputFactory(stdinPipeReader{input: os.Stdin}, pipeInputExists)

// Get is the only API provided to retrieve values from stdin json. Get
// is designed to be used in the cobra command itself, when any required
// value fed into stdin is needed. If stdin is empty or the value specified
// does not exist, Get will return nil for the value and false for the ok
// check.
func Get(inputKey string) ([]string, bool) {
if pipeInput == nil {
return nil, false
}
value, ok := pipeInput[inputKey]
return value, ok
}

// Exists is meant to be used in the init function, after GetInput is called,
// to determine if a required Cobra flag needs to be declared. If the inputKey
// exists in the Pipe module, then you can skip the required flag declaration
// and used the piped in values instead.
func Exists(inputKey string) bool {
_, ok := Get(inputKey)
return ok
}
Loading

0 comments on commit fc41f15

Please sign in to comment.