Net-5771/apply command stdin input (#19084)

* feat: apply command now accepts input from stdin

* feat: accept first positional non-flag file path arg

* fix: detect hcl format
This commit is contained in:
Poonam Jadhav 2023-10-13 09:24:16 -04:00 committed by GitHub
parent 95d9b2c7e4
commit a50a9e984a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 193 additions and 23 deletions

View File

@ -8,6 +8,7 @@ import (
"errors"
"flag"
"fmt"
"io"
"github.com/mitchellh/cli"
"google.golang.org/protobuf/encoding/protojson"
@ -32,6 +33,8 @@ type cmd struct {
help string
filePath string
testStdin io.Reader
}
func (c *cmd) init() {
@ -74,17 +77,23 @@ func (c *cmd) Run(args []string) int {
}
}
input := c.filePath
if input == "" && len(c.flags.Args()) > 0 {
input = c.flags.Arg(0)
}
var parsedResource *pbresource.Resource
if c.filePath != "" {
data, err := resource.ParseResourceFromFile(c.filePath)
if input != "" {
data, err := resource.ParseResourceInput(input, c.testStdin)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err))
return 1
}
parsedResource = data
} else {
c.UI.Error("Incorrect argument format: Flag -f with file path argument is required")
c.UI.Error("Incorrect argument format: Must provide exactly one positional argument to specify the resource to write")
return 1
}
@ -151,31 +160,42 @@ func (c *cmd) Help() string {
const synopsis = "Writes/updates resource information"
const help = `
Usage: consul resource apply -f=<file-path>
Usage: consul resource apply [options] <resource>
Write and/or update a resource by providing the definition in an hcl file as an argument
Write and/or update a resource by providing the definition. The configuration
argument is either a file path or '-' to indicate that the resource
should be read from stdin. The data should be either in HCL or
JSON form.
Example:
Example (with flag):
$ consul resource apply -f=demo.hcl
$ consul resource apply -f=demo.hcl
Sample demo.hcl:
Example (from file):
ID {
Type = gvk("group.version.kind")
Name = "resource-name"
Tenancy {
Namespace = "default"
Partition = "default"
PeerName = "local"
$ consul resource apply demo.hcl
Example (from stdin):
$ consul resource apply -
Sample demo.hcl:
ID {
Type = gvk("group.version.kind")
Name = "resource-name"
Tenancy {
Namespace = "default"
Partition = "default"
PeerName = "local"
}
}
}
Data {
Name = "demo"
}
Data {
Name = "demo"
}
Metadata = {
"foo" = "bar"
}
Metadata = {
"foo" = "bar"
}
`

View File

@ -5,12 +5,14 @@ package apply
import (
"errors"
"io"
"testing"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/command/resource/read"
"github.com/hashicorp/consul/testrpc"
)
@ -39,6 +41,11 @@ func TestResourceApplyCommand(t *testing.T) {
args: []string{"-f=../testdata/nested_data.hcl"},
output: "mesh.v2beta1.Destinations 'api' created.",
},
{
name: "file path with no flag",
args: []string{"../testdata/nested_data.hcl"},
output: "mesh.v2beta1.Destinations 'api' created.",
},
}
for _, tc := range cases {
@ -61,6 +68,129 @@ func TestResourceApplyCommand(t *testing.T) {
}
}
func readResource(t *testing.T, a *agent.TestAgent, extraArgs []string) string {
readUi := cli.NewMockUi()
readCmd := read.New(readUi)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
args = append(extraArgs, args...)
code := readCmd.Run(args)
require.Equal(t, 0, code)
require.Empty(t, readUi.ErrorWriter.String())
return readUi.OutputWriter.String()
}
func TestResourceApplyCommand_StdIn(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
a := agent.NewTestAgent(t, ``)
defer a.Shutdown()
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
t.Run("hcl", func(t *testing.T) {
stdinR, stdinW := io.Pipe()
ui := cli.NewMockUi()
c := New(ui)
c.testStdin = stdinR
stdInput := `ID {
Type = gvk("demo.v2.Artist")
Name = "korn"
Tenancy {
Namespace = "default"
Partition = "default"
PeerName = "local"
}
}
Data {
Name = "Korn"
Genre = "GENRE_METAL"
}
Metadata = {
"foo" = "bar"
}`
go func() {
stdinW.Write([]byte(stdInput))
stdinW.Close()
}()
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-",
}
code := c.Run(args)
require.Equal(t, 0, code)
require.Empty(t, ui.ErrorWriter.String())
expected := readResource(t, a, []string{"demo.v2.Artist", "korn"})
require.Contains(t, ui.OutputWriter.String(), "demo.v2.Artist 'korn' created.")
require.Contains(t, ui.OutputWriter.String(), expected)
})
t.Run("json", func(t *testing.T) {
stdinR, stdinW := io.Pipe()
ui := cli.NewMockUi()
c := New(ui)
c.testStdin = stdinR
stdInput := `{
"data": {
"genre": "GENRE_METAL",
"name": "Korn"
},
"id": {
"name": "korn",
"tenancy": {
"namespace": "default",
"partition": "default",
"peerName": "local"
},
"type": {
"group": "demo",
"groupVersion": "v2",
"kind": "Artist"
}
},
"metadata": {
"foo": "bar"
}
}`
go func() {
stdinW.Write([]byte(stdInput))
stdinW.Close()
}()
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-",
}
code := c.Run(args)
require.Equal(t, 0, code)
require.Empty(t, ui.ErrorWriter.String())
expected := readResource(t, a, []string{"demo.v2.Artist", "korn"})
require.Contains(t, ui.OutputWriter.String(), "demo.v2.Artist 'korn' created.")
require.Contains(t, ui.OutputWriter.String(), expected)
})
}
func TestResourceApplyInvalidArgs(t *testing.T) {
t.Parallel()
@ -79,7 +209,7 @@ func TestResourceApplyInvalidArgs(t *testing.T) {
"missing required flag": {
args: []string{},
expectedCode: 1,
expectedErr: errors.New("Incorrect argument format: Flag -f with file path argument is required"),
expectedErr: errors.New("Incorrect argument format: Must provide exactly one positional argument to specify the resource to write"),
},
"file parsing failure": {
args: []string{"-f=../testdata/invalid.hcl"},

View File

@ -8,6 +8,7 @@ import (
"errors"
"flag"
"fmt"
"io"
"net/http"
"strings"
"unicode"
@ -135,6 +136,25 @@ func isHCL(v []byte) bool {
return true
}
func ParseResourceInput(filePath string, stdin io.Reader) (*pbresource.Resource, error) {
data, err := helpers.LoadDataSourceNoRaw(filePath, stdin)
if err != nil {
return nil, fmt.Errorf("Failed to load data: %v", err)
}
var parsedResource *pbresource.Resource
if isHCL([]byte(data)) {
parsedResource, err = resourcehcl.Unmarshal([]byte(data), consul.NewTypeRegistry())
} else {
parsedResource, err = parseJson(data)
}
if err != nil {
return nil, fmt.Errorf("Failed to decode resource from input: %v", err)
}
return parsedResource, nil
}
func ParseInputParams(inputArgs []string, flags *flag.FlagSet) error {
if err := flags.Parse(inputArgs); err != nil {
if !errors.Is(err, flag.ErrHelp) {