diff --git a/command/registry.go b/command/registry.go index 0375787022..df7abcaef3 100644 --- a/command/registry.go +++ b/command/registry.go @@ -115,13 +115,9 @@ import ( "github.com/hashicorp/consul/command/reload" "github.com/hashicorp/consul/command/resource" resourceapply "github.com/hashicorp/consul/command/resource/apply" - resourceapplygrpc "github.com/hashicorp/consul/command/resource/apply-grpc" resourcedelete "github.com/hashicorp/consul/command/resource/delete" - resourcedeletegrpc "github.com/hashicorp/consul/command/resource/delete-grpc" resourcelist "github.com/hashicorp/consul/command/resource/list" - resourcelistgrpc "github.com/hashicorp/consul/command/resource/list-grpc" resourceread "github.com/hashicorp/consul/command/resource/read" - resourcereadgrpc "github.com/hashicorp/consul/command/resource/read-grpc" "github.com/hashicorp/consul/command/rtt" "github.com/hashicorp/consul/command/services" svcsderegister "github.com/hashicorp/consul/command/services/deregister" @@ -259,15 +255,10 @@ func RegisteredCommands(ui cli.Ui) map[string]mcli.CommandFactory { entry{"peering read", func(ui cli.Ui) (cli.Command, error) { return peerread.New(ui), nil }}, entry{"reload", func(ui cli.Ui) (cli.Command, error) { return reload.New(ui), nil }}, entry{"resource", func(cli.Ui) (cli.Command, error) { return resource.New(), nil }}, - entry{"resource read", func(ui cli.Ui) (cli.Command, error) { return resourceread.New(ui), nil }}, - entry{"resource delete", func(ui cli.Ui) (cli.Command, error) { return resourcedelete.New(ui), nil }}, entry{"resource apply", func(ui cli.Ui) (cli.Command, error) { return resourceapply.New(ui), nil }}, - // will be refactored to resource apply - entry{"resource apply-grpc", func(ui cli.Ui) (cli.Command, error) { return resourceapplygrpc.New(ui), nil }}, - entry{"resource read-grpc", func(ui cli.Ui) (cli.Command, error) { return resourcereadgrpc.New(ui), nil }}, - entry{"resource list-grpc", func(ui cli.Ui) (cli.Command, error) { return resourcelistgrpc.New(ui), nil }}, - entry{"resource delete-grpc", func(ui cli.Ui) (cli.Command, error) { return resourcedeletegrpc.New(ui), nil }}, + entry{"resource delete", func(ui cli.Ui) (cli.Command, error) { return resourcedelete.New(ui), nil }}, entry{"resource list", func(ui cli.Ui) (cli.Command, error) { return resourcelist.New(ui), nil }}, + entry{"resource read", func(ui cli.Ui) (cli.Command, error) { return resourceread.New(ui), nil }}, entry{"rtt", func(ui cli.Ui) (cli.Command, error) { return rtt.New(ui), nil }}, entry{"services", func(cli.Ui) (cli.Command, error) { return services.New(), nil }}, entry{"services register", func(ui cli.Ui) (cli.Command, error) { return svcsregister.New(ui), nil }}, diff --git a/command/resource/apply-grpc/apply.go b/command/resource/apply-grpc/apply.go deleted file mode 100644 index 1819730c48..0000000000 --- a/command/resource/apply-grpc/apply.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package apply - -import ( - "encoding/json" - "errors" - "flag" - "fmt" - "io" - - "github.com/mitchellh/cli" - - "github.com/hashicorp/consul/command/resource" - "github.com/hashicorp/consul/command/resource/client" -) - -func New(ui cli.Ui) *cmd { - c := &cmd{UI: ui} - c.init() - return c -} - -type cmd struct { - UI cli.Ui - flags *flag.FlagSet - grpcFlags *client.GRPCFlags - help string - - filePath string - - testStdin io.Reader -} - -func (c *cmd) init() { - c.flags = flag.NewFlagSet("", flag.ContinueOnError) - c.flags.StringVar(&c.filePath, "f", "", - "File path with resource definition") - - c.grpcFlags = &client.GRPCFlags{} - client.MergeFlags(c.flags, c.grpcFlags.ClientFlags()) - c.help = client.Usage(help, c.flags) -} - -func (c *cmd) Run(args []string) int { - if err := c.flags.Parse(args); err != nil { - if !errors.Is(err, flag.ErrHelp) { - c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) - return 1 - } - c.UI.Error(fmt.Sprintf("Failed to run apply command: %v", err)) - return 1 - } - - // parse resource - input := c.filePath - if input == "" { - c.UI.Error("Required '-f' flag was not provided to specify where to load the resource content from") - return 1 - } - parsedResource, 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 - } - if parsedResource == nil { - c.UI.Error("Unable to parse the file argument") - return 1 - } - - // initialize client - config, err := client.LoadGRPCConfig(nil) - if err != nil { - c.UI.Error(fmt.Sprintf("Error loading config: %s", err)) - return 1 - } - c.grpcFlags.MergeFlagsIntoGRPCConfig(config) - resourceClient, err := client.NewGRPCClient(config) - if err != nil { - c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) - return 1 - } - - // write resource - res := resource.ResourceGRPC{C: resourceClient} - entry, err := res.Apply(parsedResource) - if err != nil { - c.UI.Error(fmt.Sprintf("Error writing resource %s/%s: %v", parsedResource.Id.Type, parsedResource.Id.GetName(), err)) - return 1 - } - - // display response - b, err := json.MarshalIndent(entry, "", resource.JSON_INDENT) - if err != nil { - c.UI.Error("Failed to encode output data") - return 1 - } - c.UI.Info(fmt.Sprintf("%s.%s.%s '%s' created.", parsedResource.Id.Type.Group, parsedResource.Id.Type.GroupVersion, parsedResource.Id.Type.Kind, parsedResource.Id.GetName())) - c.UI.Info(string(b)) - - return 0 -} - -func (c *cmd) Synopsis() string { - return synopsis -} - -func (c *cmd) Help() string { - return client.Usage(c.help, nil) -} - -const synopsis = "Writes/updates resource information" - -const help = ` -Usage: consul resource apply [options] - - 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 (with flag): - - $ consul resource apply -f=demo.hcl - - Example (from stdin): - - $ consul resource apply -f - < demo.hcl - - Sample demo.hcl: - - ID { - Type = gvk("group.version.kind") - Name = "resource-name" - Tenancy { - Partition = "default" - Namespace = "default" - PeerName = "local" - } - } - - Data { - Name = "demo" - } - - Metadata = { - "foo" = "bar" - } -` diff --git a/command/resource/apply-grpc/apply_test.go b/command/resource/apply-grpc/apply_test.go deleted file mode 100644 index 7b338a0283..0000000000 --- a/command/resource/apply-grpc/apply_test.go +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package apply - -import ( - "errors" - "fmt" - "io" - "testing" - - "github.com/mitchellh/cli" - "github.com/stretchr/testify/require" - - "github.com/hashicorp/consul/agent" - "github.com/hashicorp/consul/sdk/freeport" - "github.com/hashicorp/consul/testrpc" -) - -func TestResourceApplyCommand(t *testing.T) { - if testing.Short() { - t.Skip("too slow for testing.Short") - } - - t.Parallel() - availablePort := freeport.GetOne(t) - a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) - testrpc.WaitForTestAgent(t, a.RPC, "dc1") - - t.Cleanup(func() { - a.Shutdown() - }) - - cases := []struct { - name string - output string - args []string - }{ - { - name: "sample output", - args: []string{"-f=../testdata/demo.hcl"}, - output: "demo.v2.Artist 'korn' created.", - }, - { - name: "nested data format", - args: []string{"-f=../testdata/nested_data.hcl"}, - output: "mesh.v2beta1.Destinations 'api' created.", - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - ui := cli.NewMockUi() - c := New(ui) - - args := []string{ - fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), - "-token=root", - } - - args = append(args, tc.args...) - - code := c.Run(args) - require.Equal(t, 0, code) - require.Empty(t, ui.ErrorWriter.String()) - require.Contains(t, ui.OutputWriter.String(), tc.output) - }) - } -} - -func TestResourceApplyCommand_StdIn(t *testing.T) { - if testing.Short() { - t.Skip("too slow for testing.Short") - } - - t.Parallel() - - availablePort := freeport.GetOne(t) - a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) - testrpc.WaitForTestAgent(t, a.RPC, "dc1") - - t.Cleanup(func() { - a.Shutdown() - }) - - 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 { - Partition = "default" - Namespace = "default" - } - } - - Data { - Name = "Korn" - Genre = "GENRE_METAL" - } - - Metadata = { - "foo" = "bar" - }` - - go func() { - stdinW.Write([]byte(stdInput)) - stdinW.Close() - }() - - args := []string{ - fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), - "-token=root", - "-f", - "-", - } - - code := c.Run(args) - require.Equal(t, 0, code) - require.Empty(t, ui.ErrorWriter.String()) - // Todo: make up the read result check after finishing the read command - //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": { - "partition": "default", - "namespace": "default" - }, - "type": { - "group": "demo", - "groupVersion": "v2", - "kind": "Artist" - } - }, - "metadata": { - "foo": "bar" - } - }` - - go func() { - stdinW.Write([]byte(stdInput)) - stdinW.Close() - }() - - args := []string{ - "-f", - "-", - fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), - "-token=root", - } - - code := c.Run(args) - require.Equal(t, 0, code) - require.Empty(t, ui.ErrorWriter.String()) - // Todo: make up the read result check after finishing the read command - //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() - - type tc struct { - args []string - expectedCode int - expectedErr error - } - - cases := map[string]tc{ - "no file path": { - args: []string{"-f"}, - expectedCode: 1, - expectedErr: errors.New("Failed to parse args: flag needs an argument: -f"), - }, - "missing required flag": { - args: []string{}, - expectedCode: 1, - expectedErr: errors.New("Required '-f' flag was not provided to specify where to load the resource content from"), - }, - "file parsing failure": { - args: []string{"-f=../testdata/invalid.hcl"}, - expectedCode: 1, - expectedErr: errors.New("Failed to decode resource from input file"), - }, - "file not found": { - args: []string{"-f=../testdata/test.hcl"}, - expectedCode: 1, - expectedErr: errors.New("Failed to load data: Failed to read file: open ../testdata/test.hcl: no such file or directory"), - }, - } - - for desc, tc := range cases { - t.Run(desc, func(t *testing.T) { - ui := cli.NewMockUi() - c := New(ui) - - code := c.Run(tc.args) - - require.Equal(t, tc.expectedCode, code) - require.Contains(t, ui.ErrorWriter.String(), tc.expectedErr.Error()) - }) - } -} diff --git a/command/resource/apply/apply.go b/command/resource/apply/apply.go index cf92af9474..48f238a433 100644 --- a/command/resource/apply/apply.go +++ b/command/resource/apply/apply.go @@ -11,13 +11,9 @@ import ( "io" "github.com/mitchellh/cli" - "google.golang.org/protobuf/encoding/protojson" - "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/resource" "github.com/hashicorp/consul/command/resource/client" - "github.com/hashicorp/consul/proto-public/pbresource" ) func New(ui cli.Ui) *cmd { @@ -27,10 +23,10 @@ func New(ui cli.Ui) *cmd { } type cmd struct { - UI cli.Ui - flags *flag.FlagSet - http *flags.HTTPFlags - help string + UI cli.Ui + flags *flag.FlagSet + grpcFlags *client.GRPCFlags + help string filePath string @@ -42,31 +38,9 @@ func (c *cmd) init() { c.flags.StringVar(&c.filePath, "f", "", "File path with resource definition") - c.http = &flags.HTTPFlags{} - flags.Merge(c.flags, c.http.ClientFlags()) - c.help = flags.Usage(help, c.flags) -} - -func makeWriteRequest(parsedResource *pbresource.Resource) (payload *resource.WriteRequest, error error) { - // The parsed hcl file has data field in proto message format anypb.Any - // Converting to json format requires us to fisrt marshal it then unmarshal it - data, err := protojson.Marshal(parsedResource.Data) - if err != nil { - return nil, fmt.Errorf("unrecognized hcl format: %s", err) - } - - var resourceData map[string]any - err = json.Unmarshal(data, &resourceData) - if err != nil { - return nil, fmt.Errorf("unrecognized hcl format: %s", err) - } - delete(resourceData, "@type") - - return &resource.WriteRequest{ - Data: resourceData, - Metadata: parsedResource.GetMetadata(), - Owner: parsedResource.GetOwner(), - }, nil + c.grpcFlags = &client.GRPCFlags{} + client.MergeFlags(c.flags, c.grpcFlags.ClientFlags()) + c.help = client.Usage(help, c.flags) } func (c *cmd) Run(args []string) int { @@ -75,76 +49,56 @@ func (c *cmd) Run(args []string) int { c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) return 1 } - } - - input := c.filePath - - if input == "" && len(c.flags.Args()) > 0 { - input = c.flags.Arg(0) - } - - var parsedResource *pbresource.Resource - - 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: Must provide exactly one positional argument to specify the resource to write") + c.UI.Error(fmt.Sprintf("Failed to run apply command: %v", err)) return 1 } + // parse resource + input := c.filePath + if input == "" { + c.UI.Error("Required '-f' flag was not provided to specify where to load the resource content from") + return 1 + } + parsedResource, 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 + } if parsedResource == nil { c.UI.Error("Unable to parse the file argument") return 1 } - config := api.DefaultConfig() - - c.http.MergeOntoConfig(config) - resourceClient, err := client.NewClient(config) + // initialize client + config, err := client.LoadGRPCConfig(nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Error loading config: %s", err)) + return 1 + } + c.grpcFlags.MergeFlagsIntoGRPCConfig(config) + resourceClient, err := client.NewGRPCClient(config) if err != nil { c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) return 1 } - res := resource.Resource{C: resourceClient} - - opts := &client.QueryOptions{ - Namespace: parsedResource.Id.Tenancy.GetNamespace(), - Partition: parsedResource.Id.Tenancy.GetPartition(), - Token: c.http.Token(), - } - - gvk := &resource.GVK{ - Group: parsedResource.Id.Type.GetGroup(), - Version: parsedResource.Id.Type.GetGroupVersion(), - Kind: parsedResource.Id.Type.GetKind(), - } - - writeRequest, err := makeWriteRequest(parsedResource) + // write resource + res := resource.ResourceGRPC{C: resourceClient} + entry, err := res.Apply(parsedResource) if err != nil { - c.UI.Error(fmt.Sprintf("Error parsing hcl input: %v", err)) + c.UI.Error(fmt.Sprintf("Error writing resource %s/%s: %v", parsedResource.Id.Type, parsedResource.Id.GetName(), err)) return 1 } - entry, err := res.Apply(gvk, parsedResource.Id.GetName(), opts, writeRequest) - if err != nil { - c.UI.Error(fmt.Sprintf("Error writing resource %s/%s: %v", gvk, parsedResource.Id.GetName(), err)) - return 1 - } - - b, err := json.MarshalIndent(entry, "", " ") + // display response + b, err := json.MarshalIndent(entry, "", resource.JSON_INDENT) if err != nil { c.UI.Error("Failed to encode output data") return 1 } - - c.UI.Info(fmt.Sprintf("%s.%s.%s '%s' created.", gvk.Group, gvk.Version, gvk.Kind, parsedResource.Id.GetName())) + c.UI.Info(fmt.Sprintf("%s.%s.%s '%s' created.", parsedResource.Id.Type.Group, parsedResource.Id.Type.GroupVersion, parsedResource.Id.Type.Kind, parsedResource.Id.GetName())) c.UI.Info(string(b)) + return 0 } @@ -153,7 +107,7 @@ func (c *cmd) Synopsis() string { } func (c *cmd) Help() string { - return flags.Usage(c.help, nil) + return client.Usage(c.help, nil) } const synopsis = "Writes/updates resource information" @@ -170,13 +124,9 @@ Usage: consul resource apply [options] $ consul resource apply -f=demo.hcl - Example (from file): - - $ consul resource apply demo.hcl - Example (from stdin): - $ consul resource apply - + $ consul resource apply -f - < demo.hcl Sample demo.hcl: @@ -184,8 +134,8 @@ Usage: consul resource apply [options] Type = gvk("group.version.kind") Name = "resource-name" Tenancy { - Namespace = "default" - Partition = "default" + Namespace = "default" + Partition = "default" } } diff --git a/command/resource/apply/apply_test.go b/command/resource/apply/apply_test.go index a6cb1b6929..5689c8c26f 100644 --- a/command/resource/apply/apply_test.go +++ b/command/resource/apply/apply_test.go @@ -5,6 +5,7 @@ package apply import ( "errors" + "fmt" "io" "testing" @@ -13,6 +14,7 @@ import ( "github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/command/resource/read" + "github.com/hashicorp/consul/sdk/freeport" "github.com/hashicorp/consul/testrpc" ) @@ -22,10 +24,14 @@ func TestResourceApplyCommand(t *testing.T) { } t.Parallel() - a := agent.NewTestAgent(t, ``) - defer a.Shutdown() + availablePort := freeport.GetOne(t) + a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) testrpc.WaitForTestAgent(t, a.RPC, "dc1") + t.Cleanup(func() { + a.Shutdown() + }) + cases := []struct { name string output string @@ -41,11 +47,6 @@ 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 { @@ -54,7 +55,7 @@ func TestResourceApplyCommand(t *testing.T) { c := New(ui) args := []string{ - "-http-addr=" + a.HTTPAddr(), + fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), "-token=root", } @@ -68,23 +69,6 @@ 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") @@ -92,10 +76,14 @@ func TestResourceApplyCommand_StdIn(t *testing.T) { t.Parallel() - a := agent.NewTestAgent(t, ``) - defer a.Shutdown() + availablePort := freeport.GetOne(t) + a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) testrpc.WaitForTestAgent(t, a.RPC, "dc1") + t.Cleanup(func() { + a.Shutdown() + }) + t.Run("hcl", func(t *testing.T) { stdinR, stdinW := io.Pipe() @@ -107,8 +95,8 @@ func TestResourceApplyCommand_StdIn(t *testing.T) { Type = gvk("demo.v2.Artist") Name = "korn" Tenancy { - Namespace = "default" Partition = "default" + Namespace = "default" } } @@ -127,17 +115,18 @@ func TestResourceApplyCommand_StdIn(t *testing.T) { }() args := []string{ - "-http-addr=" + a.HTTPAddr(), + fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), "-token=root", + "-f", "-", } code := c.Run(args) require.Equal(t, 0, code, ui.ErrorWriter.String()) 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) + readUI := readResource(t, []string{"demo.v2.Artist", "korn"}, availablePort) + require.Contains(t, ui.OutputWriter.String(), readUI.OutputWriter.String()) }) t.Run("json", func(t *testing.T) { @@ -155,8 +144,8 @@ func TestResourceApplyCommand_StdIn(t *testing.T) { "id": { "name": "korn", "tenancy": { - "namespace": "default", - "partition": "default" + "partition": "default", + "namespace": "default" }, "type": { "group": "demo", @@ -175,17 +164,18 @@ func TestResourceApplyCommand_StdIn(t *testing.T) { }() args := []string{ - "-http-addr=" + a.HTTPAddr(), - "-token=root", + "-f", "-", + fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), + "-token=root", } code := c.Run(args) require.Equal(t, 0, code, ui.ErrorWriter.String()) 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) + readUI := readResource(t, []string{"demo.v2.Artist", "korn"}, availablePort) + require.Contains(t, ui.OutputWriter.String(), readUI.OutputWriter.String()) }) } @@ -207,7 +197,7 @@ func TestResourceApplyInvalidArgs(t *testing.T) { "missing required flag": { args: []string{}, expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must provide exactly one positional argument to specify the resource to write"), + expectedErr: errors.New("Required '-f' flag was not provided to specify where to load the resource content from"), }, "file parsing failure": { args: []string{"-f=../testdata/invalid.hcl"}, @@ -233,3 +223,21 @@ func TestResourceApplyInvalidArgs(t *testing.T) { }) } } + +func readResource(t *testing.T, args []string, port int) *cli.MockUi { + readUi := cli.NewMockUi() + readCmd := read.New(readUi) + + flags := []string{ + fmt.Sprintf("-grpc-addr=127.0.0.1:%d", port), + "-token=root", + } + + args = append(args, flags...) + + code := readCmd.Run(args) + require.Equal(t, 0, code) + require.Empty(t, readUi.ErrorWriter.String()) + + return readUi +} diff --git a/command/resource/client/client.go b/command/resource/client/client.go deleted file mode 100644 index 7113e4e587..0000000000 --- a/command/resource/client/client.go +++ /dev/null @@ -1,999 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "net/url" - "os" - "strconv" - "strings" - "sync" - "time" - - "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/go-hclog" - - "github.com/hashicorp/consul/api" -) - -// NOTE: This client is copied from the api module to temporarily facilitate the resource cli commands - -const ( - // HTTPAddrEnvName defines an environment variable name which sets - // the HTTP address if there is no -http-addr specified. - HTTPAddrEnvName = "CONSUL_HTTP_ADDR" - - // HTTPTokenEnvName defines an environment variable name which sets - // the HTTP token. - HTTPTokenEnvName = "CONSUL_HTTP_TOKEN" - - // HTTPTokenFileEnvName defines an environment variable name which sets - // the HTTP token file. - HTTPTokenFileEnvName = "CONSUL_HTTP_TOKEN_FILE" - - // HTTPAuthEnvName defines an environment variable name which sets - // the HTTP authentication header. - HTTPAuthEnvName = "CONSUL_HTTP_AUTH" - - // HTTPSSLEnvName defines an environment variable name which sets - // whether or not to use HTTPS. - HTTPSSLEnvName = "CONSUL_HTTP_SSL" - - // HTTPCAFile defines an environment variable name which sets the - // CA file to use for talking to Consul over TLS. - HTTPCAFile = "CONSUL_CACERT" - - // HTTPCAPath defines an environment variable name which sets the - // path to a directory of CA certs to use for talking to Consul over TLS. - HTTPCAPath = "CONSUL_CAPATH" - - // HTTPClientCert defines an environment variable name which sets the - // client cert file to use for talking to Consul over TLS. - HTTPClientCert = "CONSUL_CLIENT_CERT" - - // HTTPClientKey defines an environment variable name which sets the - // client key file to use for talking to Consul over TLS. - HTTPClientKey = "CONSUL_CLIENT_KEY" - - // HTTPTLSServerName defines an environment variable name which sets the - // server name to use as the SNI host when connecting via TLS - HTTPTLSServerName = "CONSUL_TLS_SERVER_NAME" - - // HTTPSSLVerifyEnvName defines an environment variable name which sets - // whether or not to disable certificate checking. - HTTPSSLVerifyEnvName = "CONSUL_HTTP_SSL_VERIFY" - - // HTTPNamespaceEnvVar defines an environment variable name which sets - // the HTTP Namespace to be used by default. This can still be overridden. - HTTPNamespaceEnvName = "CONSUL_NAMESPACE" - - // HTTPPartitionEnvName defines an environment variable name which sets - // the HTTP Partition to be used by default. This can still be overridden. - HTTPPartitionEnvName = "CONSUL_PARTITION" - - // QueryBackendStreaming Query backend of type streaming - QueryBackendStreaming = "streaming" - - // QueryBackendBlockingQuery Query backend of type blocking query - QueryBackendBlockingQuery = "blocking-query" -) - -type StatusError struct { - Code int - Body string -} - -func (e StatusError) Error() string { - return fmt.Sprintf("Unexpected response code: %d (%s)", e.Code, e.Body) -} - -// QueryOptions are used to parameterize a query -type QueryOptions struct { - // Namespace overrides the `default` namespace - // Note: Namespaces are available only in Consul Enterprise - Namespace string - - // Partition overrides the `default` partition - // Note: Partitions are available only in Consul Enterprise - Partition string - - // Providing a datacenter overwrites the DC provided - // by the Config - Datacenter string - - // AllowStale allows any Consul server (non-leader) to service - // a read. This allows for lower latency and higher throughput - AllowStale bool - - // RequireConsistent forces the read to be fully consistent. - // This is more expensive but prevents ever performing a stale - // read. - RequireConsistent bool - - // UseCache requests that the agent cache results locally. See - // https://www.consul.io/api/features/caching.html for more details on the - // semantics. - UseCache bool - - // MaxAge limits how old a cached value will be returned if UseCache is true. - // If there is a cached response that is older than the MaxAge, it is treated - // as a cache miss and a new fetch invoked. If the fetch fails, the error is - // returned. Clients that wish to allow for stale results on error can set - // StaleIfError to a longer duration to change this behavior. It is ignored - // if the endpoint supports background refresh caching. See - // https://www.consul.io/api/features/caching.html for more details. - MaxAge time.Duration - - // StaleIfError specifies how stale the client will accept a cached response - // if the servers are unavailable to fetch a fresh one. Only makes sense when - // UseCache is true and MaxAge is set to a lower, non-zero value. It is - // ignored if the endpoint supports background refresh caching. See - // https://www.consul.io/api/features/caching.html for more details. - StaleIfError time.Duration - - // WaitIndex is used to enable a blocking query. Waits - // until the timeout or the next index is reached - WaitIndex uint64 - - // WaitHash is used by some endpoints instead of WaitIndex to perform blocking - // on state based on a hash of the response rather than a monotonic index. - // This is required when the state being blocked on is not stored in Raft, for - // example agent-local proxy configuration. - WaitHash string - - // WaitTime is used to bound the duration of a wait. - // Defaults to that of the Config, but can be overridden. - WaitTime time.Duration - - // Token is used to provide a per-request ACL token - // which overrides the agent's default token. - Token string - - // Near is used to provide a node name that will sort the results - // in ascending order based on the estimated round trip time from - // that node. Setting this to "_agent" will use the agent's node - // for the sort. - Near string - - // NodeMeta is used to filter results by nodes with the given - // metadata key/value pairs. Currently, only one key/value pair can - // be provided for filtering. - NodeMeta map[string]string - - // RelayFactor is used in keyring operations to cause responses to be - // relayed back to the sender through N other random nodes. Must be - // a value from 0 to 5 (inclusive). - RelayFactor uint8 - - // LocalOnly is used in keyring list operation to force the keyring - // query to only hit local servers (no WAN traffic). - LocalOnly bool - - // Connect filters prepared query execution to only include Connect-capable - // services. This currently affects prepared query execution. - Connect bool - - // ctx is an optional context pass through to the underlying HTTP - // request layer. Use Context() and WithContext() to manage this. - ctx context.Context - - // Filter requests filtering data prior to it being returned. The string - // is a go-bexpr compatible expression. - Filter string - - // MergeCentralConfig returns a service definition merged with the - // proxy-defaults/global and service-defaults/:service config entries. - // This can be used to ensure a full service definition is returned in the response - // especially when the service might not be written into the catalog that way. - MergeCentralConfig bool - - // Global is used to request information from all datacenters. Currently only - // used for operator usage requests. - Global bool -} - -func (o *QueryOptions) Context() context.Context { - if o != nil && o.ctx != nil { - return o.ctx - } - return context.Background() -} - -func (o *QueryOptions) WithContext(ctx context.Context) *QueryOptions { - o2 := new(QueryOptions) - if o != nil { - *o2 = *o - } - o2.ctx = ctx - return o2 -} - -// WriteOptions are used to parameterize a write -type WriteOptions struct { - // Namespace overrides the `default` namespace - // Note: Namespaces are available only in Consul Enterprise - Namespace string - - // Partition overrides the `default` partition - // Note: Partitions are available only in Consul Enterprise - Partition string - - // Providing a datacenter overwrites the DC provided - // by the Config - Datacenter string - - // Token is used to provide a per-request ACL token - // which overrides the agent's default token. - Token string - - // RelayFactor is used in keyring operations to cause responses to be - // relayed back to the sender through N other random nodes. Must be - // a value from 0 to 5 (inclusive). - RelayFactor uint8 - - // ctx is an optional context pass through to the underlying HTTP - // request layer. Use Context() and WithContext() to manage this. - ctx context.Context -} - -func (o *WriteOptions) Context() context.Context { - if o != nil && o.ctx != nil { - return o.ctx - } - return context.Background() -} - -func (o *WriteOptions) WithContext(ctx context.Context) *WriteOptions { - o2 := new(WriteOptions) - if o != nil { - *o2 = *o - } - o2.ctx = ctx - return o2 -} - -// QueryMeta is used to return meta data about a query -type QueryMeta struct { - // LastIndex. This can be used as a WaitIndex to perform - // a blocking query - LastIndex uint64 - - // LastContentHash. This can be used as a WaitHash to perform a blocking query - // for endpoints that support hash-based blocking. Endpoints that do not - // support it will return an empty hash. - LastContentHash string - - // Time of last contact from the leader for the - // server servicing the request - LastContact time.Duration - - // Is there a known leader - KnownLeader bool - - // How long did the request take - RequestTime time.Duration - - // Is address translation enabled for HTTP responses on this agent - AddressTranslationEnabled bool - - // CacheHit is true if the result was served from agent-local cache. - CacheHit bool - - // CacheAge is set if request was ?cached and indicates how stale the cached - // response is. - CacheAge time.Duration - - // QueryBackend represent which backend served the request. - QueryBackend string - - // DefaultACLPolicy is used to control the ACL interaction when there is no - // defined policy. This can be "allow" which means ACLs are used to - // deny-list, or "deny" which means ACLs are allow-lists. - DefaultACLPolicy string - - // ResultsFilteredByACLs is true when some of the query's results were - // filtered out by enforcing ACLs. It may be false because nothing was - // removed, or because the endpoint does not yet support this flag. - ResultsFilteredByACLs bool -} - -// WriteMeta is used to return meta data about a write -type WriteMeta struct { - // How long did the request take - RequestTime time.Duration -} - -// HttpBasicAuth is used to authenticate http client with HTTP Basic Authentication -type HttpBasicAuth struct { - // Username to use for HTTP Basic Authentication - Username string - - // Password to use for HTTP Basic Authentication - Password string -} - -// Config is used to configure the creation of a client -type Config struct { - // Address is the address of the Consul server - Address string - - // Scheme is the URI scheme for the Consul server - Scheme string - - // Prefix for URIs for when consul is behind an API gateway (reverse - // proxy). The API gateway must strip off the PathPrefix before - // passing the request onto consul. - PathPrefix string - - // Datacenter to use. If not provided, the default agent datacenter is used. - Datacenter string - - // Transport is the Transport to use for the http client. - Transport *http.Transport - - // HttpClient is the client to use. Default will be - // used if not provided. - HttpClient *http.Client - - // HttpAuth is the auth info to use for http access. - HttpAuth *HttpBasicAuth - - // WaitTime limits how long a Watch will block. If not provided, - // the agent default values will be used. - WaitTime time.Duration - - // Token is used to provide a per-request ACL token - // which overrides the agent's default token. - Token string - - // TokenFile is a file containing the current token to use for this client. - // If provided it is read once at startup and never again. - TokenFile string - - // Namespace is the name of the namespace to send along for the request - // when no other Namespace is present in the QueryOptions - Namespace string - - // Partition is the name of the partition to send along for the request - // when no other Partition is present in the QueryOptions - Partition string - - TLSConfig TLSConfig -} - -// TLSConfig is used to generate a TLSClientConfig that's useful for talking to -// Consul using TLS. -type TLSConfig struct { - // Address is the optional address of the Consul server. The port, if any - // will be removed from here and this will be set to the ServerName of the - // resulting config. - Address string - - // CAFile is the optional path to the CA certificate used for Consul - // communication, defaults to the system bundle if not specified. - CAFile string - - // CAPath is the optional path to a directory of CA certificates to use for - // Consul communication, defaults to the system bundle if not specified. - CAPath string - - // CAPem is the optional PEM-encoded CA certificate used for Consul - // communication, defaults to the system bundle if not specified. - CAPem []byte - - // CertFile is the optional path to the certificate for Consul - // communication. If this is set then you need to also set KeyFile. - CertFile string - - // CertPEM is the optional PEM-encoded certificate for Consul - // communication. If this is set then you need to also set KeyPEM. - CertPEM []byte - - // KeyFile is the optional path to the private key for Consul communication. - // If this is set then you need to also set CertFile. - KeyFile string - - // KeyPEM is the optional PEM-encoded private key for Consul communication. - // If this is set then you need to also set CertPEM. - KeyPEM []byte - - // InsecureSkipVerify if set to true will disable TLS host verification. - InsecureSkipVerify bool -} - -// DefaultConfig returns a default configuration for the client. By default this -// will pool and reuse idle connections to Consul. If you have a long-lived -// client object, this is the desired behavior and should make the most efficient -// use of the connections to Consul. If you don't reuse a client object, which -// is not recommended, then you may notice idle connections building up over -// time. To avoid this, use the DefaultNonPooledConfig() instead. -func DefaultConfig() *Config { - return defaultConfig(nil, cleanhttp.DefaultPooledTransport) -} - -// DefaultConfigWithLogger returns a default configuration for the client. It -// is exactly the same as DefaultConfig, but allows for a pre-configured logger -// object to be passed through. -func DefaultConfigWithLogger(logger hclog.Logger) *Config { - return defaultConfig(logger, cleanhttp.DefaultPooledTransport) -} - -// DefaultNonPooledConfig returns a default configuration for the client which -// does not pool connections. This isn't a recommended configuration because it -// will reconnect to Consul on every request, but this is useful to avoid the -// accumulation of idle connections if you make many client objects during the -// lifetime of your application. -func DefaultNonPooledConfig() *Config { - return defaultConfig(nil, cleanhttp.DefaultTransport) -} - -// defaultConfig returns the default configuration for the client, using the -// given function to make the transport. -func defaultConfig(logger hclog.Logger, transportFn func() *http.Transport) *Config { - if logger == nil { - logger = hclog.New(&hclog.LoggerOptions{ - Name: "consul-api", - }) - } - - config := &Config{ - Address: "127.0.0.1:8500", - Scheme: "http", - Transport: transportFn(), - } - - if addr := os.Getenv(HTTPAddrEnvName); addr != "" { - config.Address = addr - } - - if tokenFile := os.Getenv(HTTPTokenFileEnvName); tokenFile != "" { - config.TokenFile = tokenFile - } - - if token := os.Getenv(HTTPTokenEnvName); token != "" { - config.Token = token - } - - if auth := os.Getenv(HTTPAuthEnvName); auth != "" { - var username, password string - if strings.Contains(auth, ":") { - split := strings.SplitN(auth, ":", 2) - username = split[0] - password = split[1] - } else { - username = auth - } - - config.HttpAuth = &HttpBasicAuth{ - Username: username, - Password: password, - } - } - - if ssl := os.Getenv(HTTPSSLEnvName); ssl != "" { - enabled, err := strconv.ParseBool(ssl) - if err != nil { - logger.Warn(fmt.Sprintf("could not parse %s", HTTPSSLEnvName), "error", err) - } - - if enabled { - config.Scheme = "https" - } - } - - if v := os.Getenv(HTTPTLSServerName); v != "" { - config.TLSConfig.Address = v - } - if v := os.Getenv(HTTPCAFile); v != "" { - config.TLSConfig.CAFile = v - } - if v := os.Getenv(HTTPCAPath); v != "" { - config.TLSConfig.CAPath = v - } - if v := os.Getenv(HTTPClientCert); v != "" { - config.TLSConfig.CertFile = v - } - if v := os.Getenv(HTTPClientKey); v != "" { - config.TLSConfig.KeyFile = v - } - if v := os.Getenv(HTTPSSLVerifyEnvName); v != "" { - doVerify, err := strconv.ParseBool(v) - if err != nil { - logger.Warn(fmt.Sprintf("could not parse %s", HTTPSSLVerifyEnvName), "error", err) - } - if !doVerify { - config.TLSConfig.InsecureSkipVerify = true - } - } - - if v := os.Getenv(HTTPNamespaceEnvName); v != "" { - config.Namespace = v - } - - if v := os.Getenv(HTTPPartitionEnvName); v != "" { - config.Partition = v - } - - return config -} - -func (c *Config) GenerateEnv() []string { - env := make([]string, 0, 10) - - env = append(env, - fmt.Sprintf("%s=%s", HTTPAddrEnvName, c.Address), - fmt.Sprintf("%s=%s", HTTPTokenEnvName, c.Token), - fmt.Sprintf("%s=%s", HTTPTokenFileEnvName, c.TokenFile), - fmt.Sprintf("%s=%t", HTTPSSLEnvName, c.Scheme == "https"), - fmt.Sprintf("%s=%s", HTTPCAFile, c.TLSConfig.CAFile), - fmt.Sprintf("%s=%s", HTTPCAPath, c.TLSConfig.CAPath), - fmt.Sprintf("%s=%s", HTTPClientCert, c.TLSConfig.CertFile), - fmt.Sprintf("%s=%s", HTTPClientKey, c.TLSConfig.KeyFile), - fmt.Sprintf("%s=%s", HTTPTLSServerName, c.TLSConfig.Address), - fmt.Sprintf("%s=%t", HTTPSSLVerifyEnvName, !c.TLSConfig.InsecureSkipVerify)) - - if c.HttpAuth != nil { - env = append(env, fmt.Sprintf("%s=%s:%s", HTTPAuthEnvName, c.HttpAuth.Username, c.HttpAuth.Password)) - } else { - env = append(env, fmt.Sprintf("%s=", HTTPAuthEnvName)) - } - - return env -} - -// Client provides a client to the Consul API -type Client struct { - modifyLock sync.RWMutex - headers http.Header - - config api.Config -} - -// Headers gets the current set of headers used for requests. This returns a -// copy; to modify it call AddHeader or SetHeaders. -func (c *Client) Headers() http.Header { - c.modifyLock.RLock() - defer c.modifyLock.RUnlock() - - if c.headers == nil { - return nil - } - - ret := make(http.Header) - for k, v := range c.headers { - for _, val := range v { - ret[k] = append(ret[k], val) - } - } - - return ret -} - -// NewClient returns a new client -func NewClient(config *api.Config) (*Client, error) { - // bootstrap the config - defConfig := api.DefaultConfig() - - if config.Address == "" { - config.Address = defConfig.Address - } - - if config.Scheme == "" { - config.Scheme = defConfig.Scheme - } - - if config.Transport == nil { - config.Transport = defConfig.Transport - } - - if config.TLSConfig.Address == "" { - config.TLSConfig.Address = defConfig.TLSConfig.Address - } - - if config.TLSConfig.CAFile == "" { - config.TLSConfig.CAFile = defConfig.TLSConfig.CAFile - } - - if config.TLSConfig.CAPath == "" { - config.TLSConfig.CAPath = defConfig.TLSConfig.CAPath - } - - if config.TLSConfig.CertFile == "" { - config.TLSConfig.CertFile = defConfig.TLSConfig.CertFile - } - - if config.TLSConfig.KeyFile == "" { - config.TLSConfig.KeyFile = defConfig.TLSConfig.KeyFile - } - - if !config.TLSConfig.InsecureSkipVerify { - config.TLSConfig.InsecureSkipVerify = defConfig.TLSConfig.InsecureSkipVerify - } - - if config.HttpClient == nil { - var err error - config.HttpClient, err = NewHttpClient(config.Transport, config.TLSConfig) - if err != nil { - return nil, err - } - } - - if config.Namespace == "" { - config.Namespace = defConfig.Namespace - } - - if config.Partition == "" { - config.Partition = defConfig.Partition - } - - parts := strings.SplitN(config.Address, "://", 2) - if len(parts) == 2 { - switch parts[0] { - case "http": - // Never revert to http if TLS was explicitly requested. - case "https": - config.Scheme = "https" - case "unix": - trans := cleanhttp.DefaultTransport() - trans.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial("unix", parts[1]) - } - httpClient, err := NewHttpClient(trans, config.TLSConfig) - if err != nil { - return nil, err - } - config.HttpClient = httpClient - default: - return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0]) - } - config.Address = parts[1] - - // separate out a reverse proxy prefix, if it is present. - // NOTE: Rewriting this code to use url.Parse() instead of - // strings.SplitN() breaks existing test cases. - switch parts[0] { - case "http", "https": - parts := strings.SplitN(parts[1], "/", 2) - if len(parts) == 2 { - config.Address = parts[0] - config.PathPrefix = "/" + parts[1] - } - } - } - - // If the TokenFile is set, always use that, even if a Token is configured. - // This is because when TokenFile is set it is read into the Token field. - // We want any derived clients to have to re-read the token file. - // The precedence of ACL token should be: - // 1. -token-file cli option - // 2. -token cli option - // 3. CONSUL_HTTP_TOKEN_FILE environment variable - // 4. CONSUL_HTTP_TOKEN environment variable - if config.TokenFile != "" && config.TokenFile != defConfig.TokenFile { - data, err := os.ReadFile(config.TokenFile) - if err != nil { - return nil, fmt.Errorf("Error loading token file %s : %s", config.TokenFile, err) - } - - if token := strings.TrimSpace(string(data)); token != "" { - config.Token = token - } - } else if config.Token != "" && defConfig.Token != config.Token { - // Fall through - } else if defConfig.TokenFile != "" { - data, err := os.ReadFile(defConfig.TokenFile) - if err != nil { - return nil, fmt.Errorf("Error loading token file %s : %s", defConfig.TokenFile, err) - } - - if token := strings.TrimSpace(string(data)); token != "" { - config.Token = token - config.TokenFile = defConfig.TokenFile - } - } else { - config.Token = defConfig.Token - } - return &Client{config: *config, headers: make(http.Header)}, nil -} - -// NewHttpClient returns an http client configured with the given Transport and TLS -// config. -func NewHttpClient(transport *http.Transport, tlsConf api.TLSConfig) (*http.Client, error) { - client := &http.Client{ - Transport: transport, - } - - // TODO (slackpad) - Once we get some run time on the HTTP/2 support we - // should turn it on by default if TLS is enabled. We would basically - // just need to call http2.ConfigureTransport(transport) here. We also - // don't want to introduce another external dependency on - // golang.org/x/net/http2 at this time. For a complete recipe for how - // to enable HTTP/2 support on a transport suitable for the API client - // library see agent/http_test.go:TestHTTPServer_H2. - - if transport.TLSClientConfig == nil { - tlsClientConfig, err := api.SetupTLSConfig(&tlsConf) - - if err != nil { - return nil, err - } - - transport.TLSClientConfig = tlsClientConfig - } - - return client, nil -} - -// request is used to help build up a request -// defined a custom object that includes use-case specific config -type request struct { - config *api.Config - method string - url *url.URL - params url.Values - body io.Reader - header http.Header - Obj interface{} - ctx context.Context -} - -// setQueryOptions is used to annotate the request with -// additional query options -func (r *request) SetQueryOptions(q *QueryOptions) { - if q == nil { - return - } - if q.Namespace != "" { - // For backwards-compatibility with existing tests, - // use the short-hand query param name "ns" - // rather than the alternative long-hand "namespace" - r.params.Set("ns", q.Namespace) - } - if q.Partition != "" { - // For backwards-compatibility with existing tests, - // use the long-hand query param name "partition" - // rather than the alternative short-hand "ap" - r.params.Set("partition", q.Partition) - } - // TODO(peering/v2) handle peer tenancy - if q.Datacenter != "" { - // For backwards-compatibility with existing tests, - // use the short-hand query param name "dc" - // rather than the alternative long-hand "datacenter" - r.params.Set("dc", q.Datacenter) - } - if q.AllowStale { - r.params.Set("stale", "") - } - if q.RequireConsistent { - r.params.Set("consistent", "") - } - if q.WaitIndex != 0 { - r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10)) - } - if q.WaitTime != 0 { - r.params.Set("wait", durToMsec(q.WaitTime)) - } - if q.WaitHash != "" { - r.params.Set("hash", q.WaitHash) - } - if q.Token != "" { - r.header.Set("X-Consul-Token", q.Token) - } - if q.Near != "" { - r.params.Set("near", q.Near) - } - if q.Filter != "" { - r.params.Set("filter", q.Filter) - } - if len(q.NodeMeta) > 0 { - for key, value := range q.NodeMeta { - r.params.Add("node-meta", key+":"+value) - } - } - if q.RelayFactor != 0 { - r.params.Set("relay-factor", strconv.Itoa(int(q.RelayFactor))) - } - if q.LocalOnly { - r.params.Set("local-only", fmt.Sprintf("%t", q.LocalOnly)) - } - if q.Connect { - r.params.Set("connect", "true") - } - if q.UseCache && !q.RequireConsistent { - r.params.Set("cached", "") - - cc := []string{} - if q.MaxAge > 0 { - cc = append(cc, fmt.Sprintf("max-age=%.0f", q.MaxAge.Seconds())) - } - if q.StaleIfError > 0 { - cc = append(cc, fmt.Sprintf("stale-if-error=%.0f", q.StaleIfError.Seconds())) - } - if len(cc) > 0 { - r.header.Set("Cache-Control", strings.Join(cc, ", ")) - } - } - if q.MergeCentralConfig { - r.params.Set("merge-central-config", "") - } - if q.Global { - r.params.Set("global", "") - } - - r.ctx = q.ctx -} - -// durToMsec converts a duration to a millisecond specified string. If the -// user selected a positive value that rounds to 0 ms, then we will use 1 ms -// so they get a short delay, otherwise Consul will translate the 0 ms into -// a huge default delay. -func durToMsec(dur time.Duration) string { - ms := dur / time.Millisecond - if dur > 0 && ms == 0 { - ms = 1 - } - return fmt.Sprintf("%dms", ms) -} - -// toHTTP converts the request to an HTTP request -func (r *request) toHTTP() (*http.Request, error) { - // Encode the query parameters - r.url.RawQuery = r.params.Encode() - - // Check if we should encode the body - if r.body == nil && r.Obj != nil { - b, err := encodeBody(r.Obj) - if err != nil { - return nil, err - } - r.body = b - } - - // Create the HTTP request - req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body) - if err != nil { - return nil, err - } - - // validate that socket communications that do not use the host, detect - // slashes in the host name and replace it with local host. - // this is required since go started validating req.host in 1.20.6 and 1.19.11. - // prior to that they would strip out the slashes for you. They removed that - // behavior and added more strict validation as part of a CVE. - // This issue is being tracked by the Go team: - // https://github.com/golang/go/issues/61431 - // If there is a resolution in this issue, we will remove this code. - // In the time being, this is the accepted workaround. - if strings.HasPrefix(r.url.Host, "/") { - r.url.Host = "localhost" - } - - req.URL.Host = r.url.Host - req.URL.Scheme = r.url.Scheme - req.Host = r.url.Host - req.Header = r.header - - // Content-Type must always be set when a body is present - // See https://github.com/hashicorp/consul/issues/10011 - if req.Body != nil && req.Header.Get("Content-Type") == "" { - req.Header.Set("Content-Type", "application/json") - } - - // Setup auth - if r.config.HttpAuth != nil { - req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password) - } - if r.ctx != nil { - return req.WithContext(r.ctx), nil - } - - return req, nil -} - -// newRequest is used to create a new request -func (c *Client) NewRequest(method, path string) *request { - r := &request{ - config: &c.config, - method: method, - url: &url.URL{ - Scheme: c.config.Scheme, - Host: c.config.Address, - Path: c.config.PathPrefix + path, - }, - params: make(map[string][]string), - header: c.Headers(), - } - - if c.config.Datacenter != "" { - r.params.Set("dc", c.config.Datacenter) - } - if c.config.Namespace != "" { - r.params.Set("ns", c.config.Namespace) - } - if c.config.Partition != "" { - r.params.Set("partition", c.config.Partition) - } - if c.config.WaitTime != 0 { - r.params.Set("wait", durToMsec(r.config.WaitTime)) - } - if c.config.Token != "" { - r.header.Set("X-Consul-Token", r.config.Token) - } - return r -} - -// doRequest runs a request with our client -func (c *Client) DoRequest(r *request) (time.Duration, *http.Response, error) { - req, err := r.toHTTP() - if err != nil { - return 0, nil, err - } - start := time.Now() - resp, err := c.config.HttpClient.Do(req) - diff := time.Since(start) - return diff, resp, err -} - -// DecodeBody is used to JSON decode a body -func DecodeBody(resp *http.Response, out interface{}) error { - dec := json.NewDecoder(resp.Body) - return dec.Decode(out) -} - -// encodeBody is used to encode a request body -func encodeBody(obj interface{}) (io.Reader, error) { - buf := bytes.NewBuffer(nil) - enc := json.NewEncoder(buf) - if err := enc.Encode(obj); err != nil { - return nil, err - } - return buf, nil -} - -// requireOK is used to wrap doRequest and check for a 200 -func RequireOK(resp *http.Response) error { - return RequireHttpCodes(resp, 200) -} - -// requireHttpCodes checks for the "allowable" http codes for a response -func RequireHttpCodes(resp *http.Response, httpCodes ...int) error { - // if there is an http code that we require, return w no error - for _, httpCode := range httpCodes { - if resp.StatusCode == httpCode { - return nil - } - } - - // if we reached here, then none of the http codes in resp matched any that we expected - // so err out - return generateUnexpectedResponseCodeError(resp) -} - -// closeResponseBody reads resp.Body until EOF, and then closes it. The read -// is necessary to ensure that the http.Client's underlying RoundTripper is able -// to re-use the TCP connection. See godoc on net/http.Client.Do. -func CloseResponseBody(resp *http.Response) error { - _, _ = io.Copy(io.Discard, resp.Body) - return resp.Body.Close() -} - -// generateUnexpectedResponseCodeError consumes the rest of the body, closes -// the body stream and generates an error indicating the status code was -// unexpected. -func generateUnexpectedResponseCodeError(resp *http.Response) error { - var buf bytes.Buffer - io.Copy(&buf, resp.Body) - CloseResponseBody(resp) - - trimmed := strings.TrimSpace(string(buf.Bytes())) - return StatusError{Code: resp.StatusCode, Body: trimmed} -} diff --git a/command/resource/client/grpc-resource-flags.go b/command/resource/client/grpc-resource-flags.go index 0671b0be82..576b39744d 100644 --- a/command/resource/client/grpc-resource-flags.go +++ b/command/resource/client/grpc-resource-flags.go @@ -8,7 +8,6 @@ import "flag" type ResourceFlags struct { partition TValue[string] namespace TValue[string] - peername TValue[string] stale TValue[bool] } @@ -21,7 +20,6 @@ func (f *ResourceFlags) ResourceFlags() *flag.FlagSet { fs.Var(&f.namespace, "namespace", "Specifies the namespace to query. If not provided, the namespace will be inferred "+ "from the request's ACL token, or will default to the `default` namespace.") - fs.Var(&f.peername, "peer", "Specifies the name of peer to query. By default, it is `local`.") fs.Var(&f.stale, "stale", "Permit any Consul server (non-leader) to respond to this request. This "+ "allows for lower latency and higher throughput, but can result in "+ @@ -38,10 +36,6 @@ func (f *ResourceFlags) Namespace() string { return f.namespace.String() } -func (f *ResourceFlags) Peername() string { - return f.peername.String() -} - func (f *ResourceFlags) Stale() bool { if f.stale.v == nil { return false diff --git a/command/resource/delete-grpc/delete.go b/command/resource/delete-grpc/delete.go deleted file mode 100644 index c924a6bb9d..0000000000 --- a/command/resource/delete-grpc/delete.go +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package delete - -import ( - "errors" - "flag" - "fmt" - - "github.com/mitchellh/cli" - - "github.com/hashicorp/consul/command/flags" - "github.com/hashicorp/consul/command/resource" - "github.com/hashicorp/consul/command/resource/client" - "github.com/hashicorp/consul/proto-public/pbresource" -) - -func New(ui cli.Ui) *cmd { - c := &cmd{UI: ui} - c.init() - return c -} - -type cmd struct { - UI cli.Ui - flags *flag.FlagSet - grpcFlags *client.GRPCFlags - resourceFlags *client.ResourceFlags - help string - - filePath string -} - -func (c *cmd) init() { - c.flags = flag.NewFlagSet("", flag.ContinueOnError) - c.flags.StringVar(&c.filePath, "f", "", - "File path with resource definition") - - c.grpcFlags = &client.GRPCFlags{} - c.resourceFlags = &client.ResourceFlags{} - client.MergeFlags(c.flags, c.grpcFlags.ClientFlags()) - client.MergeFlags(c.flags, c.resourceFlags.ResourceFlags()) - c.help = client.Usage(help, c.flags) -} - -func (c *cmd) Run(args []string) int { - var resourceType *pbresource.Type - var resourceTenancy *pbresource.Tenancy - var resourceName string - - if err := c.flags.Parse(args); err != nil { - if !errors.Is(err, flag.ErrHelp) { - c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) - return 1 - } - c.UI.Error(fmt.Sprintf("Failed to run delete command: %v", err)) - return 1 - } - - // collect resource type, name and tenancy - if c.flags.Lookup("f").Value.String() != "" { - if c.filePath == "" { - c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) - return 1 - } - parsedResource, err := resource.ParseResourceFromFile(c.filePath) - if err != nil { - c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) - return 1 - } - - if parsedResource == nil { - c.UI.Error("Unable to parse the file argument") - return 1 - } - - resourceType = parsedResource.Id.Type - resourceTenancy = parsedResource.Id.Tenancy - resourceName = parsedResource.Id.Name - } else { - var err error - resourceType, resourceName, err = resource.GetTypeAndResourceName(args) - if err != nil { - c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) - return 1 - } - - inputArgs := args[2:] - err = resource.ParseInputParams(inputArgs, c.flags) - if err != nil { - c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err)) - return 1 - } - if c.filePath != "" { - c.UI.Error("Incorrect argument format: File argument is not needed when resource information is provided with the command") - return 1 - } - resourceTenancy = &pbresource.Tenancy{ - Partition: c.resourceFlags.Partition(), - Namespace: c.resourceFlags.Namespace(), - } - } - - // initialize client - config, err := client.LoadGRPCConfig(nil) - if err != nil { - c.UI.Error(fmt.Sprintf("Error loading config: %s", err)) - return 1 - } - c.grpcFlags.MergeFlagsIntoGRPCConfig(config) - resourceClient, err := client.NewGRPCClient(config) - if err != nil { - c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) - return 1 - } - - // delete resource - res := resource.ResourceGRPC{C: resourceClient} - err = res.Delete(resourceType, resourceTenancy, resourceName) - if err != nil { - c.UI.Error(fmt.Sprintf("Error deleting resource %s/%s: %v", resourceType, resourceName, err)) - return 1 - } - - c.UI.Info(fmt.Sprintf("%s.%s.%s/%s deleted", resourceType.Group, resourceType.GroupVersion, resourceType.Kind, resourceName)) - return 0 -} - -func (c *cmd) Synopsis() string { - return synopsis -} - -func (c *cmd) Help() string { - return flags.Usage(c.help, nil) -} - -const synopsis = "Delete resource information" -const help = ` -Usage: You have two options to delete the resource specified by the given -type, name, partition, namespace and peer and outputs its JSON representation. - -consul resource delete [type] [name] -partition= -namespace= -peer= -consul resource delete -f [resource_file_path] - -But you could only use one of the approaches. - -Example: - -$ consul resource delete catalog.v2beta1.Service card-processor -partition=billing -namespace=payments -peer=eu -$ consul resource delete -f resource.hcl - -In resource.hcl, it could be: -ID { - Type = gvk("catalog.v2beta1.Service") - Name = "card-processor" - Tenancy { - Partition = "billing" - Namespace = "payments" - PeerName = "eu" - } -} -` diff --git a/command/resource/delete-grpc/delete_test.go b/command/resource/delete-grpc/delete_test.go deleted file mode 100644 index 0bab59667a..0000000000 --- a/command/resource/delete-grpc/delete_test.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 -package delete - -import ( - "errors" - "fmt" - "testing" - - "github.com/mitchellh/cli" - "github.com/stretchr/testify/require" - - "github.com/hashicorp/consul/agent" - "github.com/hashicorp/consul/command/resource/apply-grpc" - "github.com/hashicorp/consul/sdk/freeport" - "github.com/hashicorp/consul/testrpc" -) - -func TestResourceDeleteInvalidArgs(t *testing.T) { - t.Parallel() - - type tc struct { - args []string - expectedCode int - expectedErr error - } - - cases := map[string]tc{ - "nil args": { - args: nil, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must specify two arguments: resource type and resource name"), - }, - "empty args": { - args: []string{}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must specify two arguments: resource type and resource name"), - }, - "missing file path": { - args: []string{"-f"}, - expectedCode: 1, - expectedErr: errors.New("Failed to parse args: flag needs an argument: -f"), - }, - "file not found": { - args: []string{"-f=../testdata/test.hcl"}, - expectedCode: 1, - expectedErr: errors.New("Failed to load data: Failed to read file: open ../testdata/test.hcl: no such file or directory"), - }, - "provide type and name": { - args: []string{"a.b.c"}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must specify two arguments: resource type and resource name"), - }, - "provide type and name with -f": { - args: []string{"a.b.c", "name", "-f", "test.hcl"}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: File argument is not needed when resource information is provided with the command"), - }, - "provide type and name with -f and other flags": { - args: []string{"a.b.c", "name", "-f", "test.hcl", "-namespace", "default"}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: File argument is not needed when resource information is provided with the command"), - }, - "does not provide resource name after type": { - args: []string{"a.b.c", "-namespace", "default"}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must provide resource name right after type"), - }, - "invalid resource type format": { - args: []string{"a.", "name", "-namespace", "default"}, - expectedCode: 1, - expectedErr: errors.New("Must provide resource type argument with either in group.version.kind format or its shorthand name"), - }, - } - - for desc, tc := range cases { - t.Run(desc, func(t *testing.T) { - ui := cli.NewMockUi() - c := New(ui) - - code := c.Run(tc.args) - - require.Equal(t, tc.expectedCode, code) - require.Contains(t, ui.ErrorWriter.String(), tc.expectedErr.Error()) - }) - } -} - -func createResource(t *testing.T, port int) { - applyUi := cli.NewMockUi() - applyCmd := apply.New(applyUi) - - args := []string{ - fmt.Sprintf("-grpc-addr=127.0.0.1:%d", port), - "-token=root", - } - - args = append(args, []string{"-f=../testdata/demo.hcl"}...) - - code := applyCmd.Run(args) - require.Equal(t, 0, code) - require.Empty(t, applyUi.ErrorWriter.String()) -} - -func TestResourceDelete(t *testing.T) { - if testing.Short() { - t.Skip("too slow for testing.Short") - } - - t.Parallel() - - availablePort := freeport.GetOne(t) - a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) - testrpc.WaitForTestAgent(t, a.RPC, "dc1") - t.Cleanup(func() { - a.Shutdown() - }) - - defaultCmdArgs := []string{ - fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), - "-token=root", - } - - cases := []struct { - name string - args []string - expectedCode int - createResource bool - }{ - { - name: "delete resource in hcl format", - args: []string{"-f=../testdata/demo.hcl"}, - expectedCode: 0, - createResource: true, - }, - { - name: "delete resource in command line format", - args: []string{"demo.v2.Artist", "korn", "-partition=default", "-namespace=default", "-peer=local"}, - expectedCode: 0, - createResource: true, - }, - { - name: "delete resource that doesn't exist in command line format", - args: []string{"demo.v2.Artist", "korn", "-partition=default", "-namespace=default", "-peer=local"}, - expectedCode: 0, - createResource: false, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - ui := cli.NewMockUi() - c := New(ui) - cliArgs := append(tc.args, defaultCmdArgs...) - if tc.createResource { - createResource(t, availablePort) - } - code := c.Run(cliArgs) - require.Empty(t, ui.ErrorWriter.String()) - require.Equal(t, tc.expectedCode, code) - require.Contains(t, ui.OutputWriter.String(), "deleted") - }) - } -} diff --git a/command/resource/delete/delete.go b/command/resource/delete/delete.go index c0750210fe..bf371f7af5 100644 --- a/command/resource/delete/delete.go +++ b/command/resource/delete/delete.go @@ -10,7 +10,6 @@ import ( "github.com/mitchellh/cli" - "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/resource" "github.com/hashicorp/consul/command/resource/client" @@ -24,75 +23,64 @@ func New(ui cli.Ui) *cmd { } type cmd struct { - UI cli.Ui - flags *flag.FlagSet - http *flags.HTTPFlags - help string + UI cli.Ui + flags *flag.FlagSet + grpcFlags *client.GRPCFlags + resourceFlags *client.ResourceFlags + help string filePath string } func (c *cmd) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) - c.http = &flags.HTTPFlags{} - c.flags.StringVar(&c.filePath, "f", "", "File path with resource definition") - flags.Merge(c.flags, c.http.ClientFlags()) - flags.Merge(c.flags, c.http.ServerFlags()) - flags.Merge(c.flags, c.http.MultiTenancyFlags()) - // TODO(peering/v2) add back ability to query peers - // flags.Merge(c.flags, c.http.AddPeerName()) - c.help = flags.Usage(help, c.flags) + c.flags.StringVar(&c.filePath, "f", "", + "File path with resource definition") + + c.grpcFlags = &client.GRPCFlags{} + c.resourceFlags = &client.ResourceFlags{} + client.MergeFlags(c.flags, c.grpcFlags.ClientFlags()) + client.MergeFlags(c.flags, c.resourceFlags.ResourceFlags()) + c.help = client.Usage(help, c.flags) } func (c *cmd) Run(args []string) int { - var gvk *resource.GVK + var resourceType *pbresource.Type + var resourceTenancy *pbresource.Tenancy var resourceName string - var opts *client.QueryOptions if err := c.flags.Parse(args); err != nil { if !errors.Is(err, flag.ErrHelp) { c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) return 1 } + c.UI.Error(fmt.Sprintf("Failed to run delete command: %v", err)) + return 1 } + // collect resource type, name and tenancy if c.flags.Lookup("f").Value.String() != "" { - if c.filePath != "" { - parsedResource, err := resource.ParseResourceFromFile(c.filePath) - if err != nil { - c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) - return 1 - } - - if parsedResource == nil { - c.UI.Error("Unable to parse the file argument") - return 1 - } - - gvk = &resource.GVK{ - Group: parsedResource.Id.Type.GetGroup(), - Version: parsedResource.Id.Type.GetGroupVersion(), - Kind: parsedResource.Id.Type.GetKind(), - } - resourceName = parsedResource.Id.GetName() - opts = &client.QueryOptions{ - Namespace: parsedResource.Id.Tenancy.GetNamespace(), - Partition: parsedResource.Id.Tenancy.GetPartition(), - Token: c.http.Token(), - } - } else { + if c.filePath == "" { c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) return 1 } + parsedResource, err := resource.ParseResourceFromFile(c.filePath) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) + return 1 + } + + if parsedResource == nil { + c.UI.Error("Unable to parse the file argument") + return 1 + } + + resourceType = parsedResource.Id.Type + resourceTenancy = parsedResource.Id.Tenancy + resourceName = parsedResource.Id.Name } else { var err error - var resourceType *pbresource.Type resourceType, resourceName, err = resource.GetTypeAndResourceName(args) - gvk = &resource.GVK{ - Group: resourceType.GetGroup(), - Version: resourceType.GetGroupVersion(), - Kind: resourceType.GetKind(), - } if err != nil { c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) return 1 @@ -108,30 +96,34 @@ func (c *cmd) Run(args []string) int { c.UI.Error("Incorrect argument format: File argument is not needed when resource information is provided with the command") return 1 } - opts = &client.QueryOptions{ - Namespace: c.http.Namespace(), - Partition: c.http.Partition(), - Token: c.http.Token(), + resourceTenancy = &pbresource.Tenancy{ + Partition: c.resourceFlags.Partition(), + Namespace: c.resourceFlags.Namespace(), } } - config := api.DefaultConfig() - - c.http.MergeOntoConfig(config) - resourceClient, err := client.NewClient(config) + // initialize client + config, err := client.LoadGRPCConfig(nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Error loading config: %s", err)) + return 1 + } + c.grpcFlags.MergeFlagsIntoGRPCConfig(config) + resourceClient, err := client.NewGRPCClient(config) if err != nil { c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) return 1 } - res := resource.Resource{C: resourceClient} - - if err := res.Delete(gvk, resourceName, opts); err != nil { - c.UI.Error(fmt.Sprintf("Error deleting resource %s.%s.%s/%s: %v", gvk.Group, gvk.Version, gvk.Kind, resourceName, err)) + // delete resource + res := resource.ResourceGRPC{C: resourceClient} + err = res.Delete(resourceType, resourceTenancy, resourceName) + if err != nil { + c.UI.Error(fmt.Sprintf("Error deleting resource %s/%s: %v", resourceType, resourceName, err)) return 1 } - c.UI.Info(fmt.Sprintf("%s.%s.%s/%s deleted", gvk.Group, gvk.Version, gvk.Kind, resourceName)) + c.UI.Info(fmt.Sprintf("%s.%s.%s/%s deleted", resourceType.Group, resourceType.GroupVersion, resourceType.Kind, resourceName)) return 0 } @@ -146,7 +138,7 @@ func (c *cmd) Help() string { const synopsis = "Delete resource information" const help = ` Usage: You have two options to delete the resource specified by the given -type, name, partition, namespace and peer and outputs its JSON representation. +type, name, partition, namespace and outputs its JSON representation. consul resource delete [type] [name] -partition= -namespace= consul resource delete -f [resource_file_path] @@ -160,11 +152,11 @@ $ consul resource delete -f resource.hcl In resource.hcl, it could be: ID { - Type = gvk("catalog.v2beta1.Service") - Name = "card-processor" - Tenancy { - Namespace = "payments" - Partition = "billing" - } + Type = gvk("catalog.v2beta1.Service") + Name = "card-processor" + Tenancy { + Partition = "billing" + Namespace = "payments" + } } ` diff --git a/command/resource/delete/delete_test.go b/command/resource/delete/delete_test.go index 8e12aa486d..30874f8d14 100644 --- a/command/resource/delete/delete_test.go +++ b/command/resource/delete/delete_test.go @@ -4,6 +4,7 @@ package delete import ( "errors" + "fmt" "testing" "github.com/mitchellh/cli" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/command/resource/apply" + "github.com/hashicorp/consul/sdk/freeport" "github.com/hashicorp/consul/testrpc" ) @@ -84,12 +86,12 @@ func TestResourceDeleteInvalidArgs(t *testing.T) { } } -func createResource(t *testing.T, a *agent.TestAgent) { +func createResource(t *testing.T, port int) { applyUi := cli.NewMockUi() applyCmd := apply.New(applyUi) args := []string{ - "-http-addr=" + a.HTTPAddr(), + fmt.Sprintf("-grpc-addr=127.0.0.1:%d", port), "-token=root", } @@ -107,14 +109,18 @@ func TestResourceDelete(t *testing.T) { t.Parallel() - a := agent.NewTestAgent(t, ``) - defer a.Shutdown() + availablePort := freeport.GetOne(t) + a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) testrpc.WaitForTestAgent(t, a.RPC, "dc1") + t.Cleanup(func() { + a.Shutdown() + }) defaultCmdArgs := []string{ - "-http-addr=" + a.HTTPAddr(), + fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), "-token=root", } + cases := []struct { name string args []string @@ -147,7 +153,7 @@ func TestResourceDelete(t *testing.T) { c := New(ui) cliArgs := append(tc.args, defaultCmdArgs...) if tc.createResource { - createResource(t, a) + createResource(t, availablePort) } code := c.Run(cliArgs) require.Empty(t, ui.ErrorWriter.String()) diff --git a/command/resource/helper.go b/command/resource/helper.go index 69a06d9cc1..d541cdd125 100644 --- a/command/resource/helper.go +++ b/command/resource/helper.go @@ -9,7 +9,6 @@ import ( "flag" "fmt" "io" - "net/http" "strings" "unicode" "unicode/utf8" @@ -19,7 +18,6 @@ import ( "github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/command/helpers" - "github.com/hashicorp/consul/command/resource/client" "github.com/hashicorp/consul/internal/resourcehcl" "github.com/hashicorp/consul/proto-public/pbresource" ) @@ -40,8 +38,6 @@ type Tenancy struct { Namespace string `json:"namespace"` } -// TODO(peering/v2) handle v2 peering in the resource cli - type Type struct { Group string `json:"group"` GroupVersion string `json:"groupVersion"` @@ -55,76 +51,10 @@ type ID struct { UID string `json:"uid"` } -func parseJson(js string) (*pbresource.Resource, error) { - - parsedResource := new(pbresource.Resource) - - var outerResource OuterResource - - if err := json.Unmarshal([]byte(js), &outerResource); err != nil { - return nil, err - } - - if outerResource.ID == nil { - return nil, fmt.Errorf("\"id\" field need to be provided") - } - - typ := pbresource.Type{ - Kind: outerResource.ID.Type.Kind, - Group: outerResource.ID.Type.Group, - GroupVersion: outerResource.ID.Type.GroupVersion, - } - - reg, ok := consul.NewTypeRegistry().Resolve(&typ) - if !ok { - return nil, fmt.Errorf("invalid type %v", parsedResource) - } - data := reg.Proto.ProtoReflect().New().Interface() - anyProtoMsg, err := anypb.New(data) - if err != nil { - return nil, err - } - - outerResource.Data["@type"] = anyProtoMsg.TypeUrl - - marshal, err := json.Marshal(outerResource) - if err != nil { - return nil, err - } - - if err := protojson.Unmarshal(marshal, parsedResource); err != nil { - return nil, err - } - return parsedResource, nil -} - func ParseResourceFromFile(filePath string) (*pbresource.Resource, error) { return ParseResourceInput(filePath, nil) } -// this is an inlined variant of hcl.lexMode() -func isHCL(v []byte) bool { - var ( - r rune - w int - offset int - ) - - for { - r, w = utf8.DecodeRune(v[offset:]) - offset += w - if unicode.IsSpace(r) { - continue - } - if r == '{' { - return false - } - break - } - - return true -} - func ParseResourceInput(filePath string, stdin io.Reader) (*pbresource.Resource, error) { data, err := helpers.LoadDataSourceNoRaw(filePath, stdin) @@ -168,108 +98,6 @@ func GetTypeAndResourceName(args []string) (resourceType *pbresource.Type, resou return resourceType, resourceName, e } -type Resource struct { - C *client.Client -} - -type GVK struct { - Group string - Version string - Kind string -} - -type WriteRequest struct { - Metadata map[string]string `json:"metadata"` - Data map[string]any `json:"data"` - Owner *pbresource.ID `json:"owner"` -} - -type ListResponse struct { - Resources []map[string]interface{} `json:"resources"` -} - -func (gvk *GVK) String() string { - return fmt.Sprintf("%s.%s.%s", gvk.Group, gvk.Version, gvk.Kind) -} - -func (resource *Resource) Read(gvk *GVK, resourceName string, q *client.QueryOptions) (map[string]interface{}, error) { - r := resource.C.NewRequest("GET", strings.ToLower(fmt.Sprintf("/api/%s/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind, resourceName))) - r.SetQueryOptions(q) - _, resp, err := resource.C.DoRequest(r) - if err != nil { - return nil, err - } - defer client.CloseResponseBody(resp) - if err := client.RequireOK(resp); err != nil { - return nil, err - } - - var out map[string]interface{} - if err := client.DecodeBody(resp, &out); err != nil { - return nil, err - } - - return out, nil -} - -func (resource *Resource) Delete(gvk *GVK, resourceName string, q *client.QueryOptions) error { - r := resource.C.NewRequest("DELETE", strings.ToLower(fmt.Sprintf("/api/%s/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind, resourceName))) - r.SetQueryOptions(q) - _, resp, err := resource.C.DoRequest(r) - if err != nil { - return err - } - defer client.CloseResponseBody(resp) - if err := client.RequireHttpCodes(resp, http.StatusNoContent); err != nil { - return err - } - return nil -} - -func (resource *Resource) Apply(gvk *GVK, resourceName string, q *client.QueryOptions, payload *WriteRequest) (*map[string]interface{}, error) { - url := strings.ToLower(fmt.Sprintf("/api/%s/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind, resourceName)) - - r := resource.C.NewRequest("PUT", url) - r.SetQueryOptions(q) - r.Obj = payload - _, resp, err := resource.C.DoRequest(r) - if err != nil { - return nil, err - } - defer client.CloseResponseBody(resp) - if err := client.RequireOK(resp); err != nil { - return nil, err - } - - var out map[string]interface{} - - if err := client.DecodeBody(resp, &out); err != nil { - return nil, err - } - - return &out, nil -} - -func (resource *Resource) List(gvk *GVK, q *client.QueryOptions) (*ListResponse, error) { - r := resource.C.NewRequest("GET", strings.ToLower(fmt.Sprintf("/api/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind))) - r.SetQueryOptions(q) - _, resp, err := resource.C.DoRequest(r) - if err != nil { - return nil, err - } - defer client.CloseResponseBody(resp) - if err := client.RequireOK(resp); err != nil { - return nil, err - } - - var out *ListResponse - if err := client.DecodeBody(resp, &out); err != nil { - return nil, err - } - - return out, nil -} - func InferTypeFromResourceType(resourceType string) (*pbresource.Type, error) { s := strings.Split(resourceType, ".") switch length := len(s); { @@ -320,3 +148,69 @@ func BuildKindToGVKMap() map[string][]string { } return kindToGVKMap } + +// this is an inlined variant of hcl.lexMode() +func isHCL(v []byte) bool { + var ( + r rune + w int + offset int + ) + + for { + r, w = utf8.DecodeRune(v[offset:]) + offset += w + if unicode.IsSpace(r) { + continue + } + if r == '{' { + return false + } + break + } + + return true +} + +func parseJson(js string) (*pbresource.Resource, error) { + + parsedResource := new(pbresource.Resource) + + var outerResource OuterResource + + if err := json.Unmarshal([]byte(js), &outerResource); err != nil { + return nil, err + } + + if outerResource.ID == nil { + return nil, fmt.Errorf("\"id\" field need to be provided") + } + + typ := pbresource.Type{ + Kind: outerResource.ID.Type.Kind, + Group: outerResource.ID.Type.Group, + GroupVersion: outerResource.ID.Type.GroupVersion, + } + + reg, ok := consul.NewTypeRegistry().Resolve(&typ) + if !ok { + return nil, fmt.Errorf("invalid type %v", parsedResource) + } + data := reg.Proto.ProtoReflect().New().Interface() + anyProtoMsg, err := anypb.New(data) + if err != nil { + return nil, err + } + + outerResource.Data["@type"] = anyProtoMsg.TypeUrl + + marshal, err := json.Marshal(outerResource) + if err != nil { + return nil, err + } + + if err := protojson.Unmarshal(marshal, parsedResource); err != nil { + return nil, err + } + return parsedResource, nil +} diff --git a/command/resource/list-grpc/list.go b/command/resource/list-grpc/list.go deleted file mode 100644 index eea7621b9e..0000000000 --- a/command/resource/list-grpc/list.go +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package list - -import ( - "encoding/json" - "errors" - "flag" - "fmt" - "strings" - - "github.com/mitchellh/cli" - - "github.com/hashicorp/consul/command/flags" - "github.com/hashicorp/consul/command/resource" - "github.com/hashicorp/consul/command/resource/client" - "github.com/hashicorp/consul/proto-public/pbresource" -) - -func New(ui cli.Ui) *cmd { - c := &cmd{UI: ui} - c.init() - return c -} - -type cmd struct { - UI cli.Ui - flags *flag.FlagSet - grpcFlags *client.GRPCFlags - resourceFlags *client.ResourceFlags - help string - - filePath string - prefix string -} - -func (c *cmd) init() { - c.flags = flag.NewFlagSet("", flag.ContinueOnError) - c.flags.StringVar(&c.filePath, "f", "", - "File path with resource definition") - c.flags.StringVar(&c.prefix, "p", "", - "Name prefix for listing resources if you need ambiguous match") - - c.grpcFlags = &client.GRPCFlags{} - c.resourceFlags = &client.ResourceFlags{} - client.MergeFlags(c.flags, c.grpcFlags.ClientFlags()) - client.MergeFlags(c.flags, c.resourceFlags.ResourceFlags()) - c.help = client.Usage(help, c.flags) -} - -func (c *cmd) Run(args []string) int { - var resourceType *pbresource.Type - var resourceTenancy *pbresource.Tenancy - - if err := c.flags.Parse(args); err != nil { - if !errors.Is(err, flag.ErrHelp) { - c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) - return 1 - } - c.UI.Error(fmt.Sprintf("Failed to run list command: %v", err)) - return 1 - } - - // collect resource type, name and tenancy - if c.flags.Lookup("f").Value.String() != "" { - if c.filePath == "" { - c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) - return 1 - } - parsedResource, err := resource.ParseResourceFromFile(c.filePath) - if err != nil { - c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) - return 1 - } - - if parsedResource == nil { - c.UI.Error("Unable to parse the file argument") - return 1 - } - - resourceType = parsedResource.Id.Type - resourceTenancy = parsedResource.Id.Tenancy - } else { - var err error - args := c.flags.Args() - if err = validateArgs(args); err != nil { - c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) - return 1 - } - resourceType, err = resource.InferTypeFromResourceType(args[0]) - if err != nil { - c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) - return 1 - } - - // skip resource type to parse remaining args - inputArgs := c.flags.Args()[1:] - err = resource.ParseInputParams(inputArgs, c.flags) - if err != nil { - c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err)) - return 1 - } - if c.filePath != "" { - c.UI.Error("Incorrect argument format: File argument is not needed when resource information is provided with the command") - return 1 - } - resourceTenancy = &pbresource.Tenancy{ - Partition: c.resourceFlags.Partition(), - Namespace: c.resourceFlags.Namespace(), - } - } - - // initialize client - config, err := client.LoadGRPCConfig(nil) - if err != nil { - c.UI.Error(fmt.Sprintf("Error loading config: %s", err)) - return 1 - } - c.grpcFlags.MergeFlagsIntoGRPCConfig(config) - resourceClient, err := client.NewGRPCClient(config) - if err != nil { - c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) - return 1 - } - - // list resource - res := resource.ResourceGRPC{C: resourceClient} - entry, err := res.List(resourceType, resourceTenancy, c.prefix, c.resourceFlags.Stale()) - if err != nil { - c.UI.Error(fmt.Sprintf("Error listing resource %s/%s: %v", resourceType, c.prefix, err)) - return 1 - } - - // display response - b, err := json.MarshalIndent(entry, "", resource.JSON_INDENT) - if err != nil { - c.UI.Error("Failed to encode output data") - return 1 - } - - c.UI.Info(string(b)) - return 0 -} - -func validateArgs(args []string) error { - if args == nil { - return fmt.Errorf("Must include resource type or flag arguments") - } - if len(args) < 1 { - return fmt.Errorf("Must include resource type argument") - } - if len(args) > 1 && !strings.HasPrefix(args[1], "-") { - return fmt.Errorf("Must include flag arguments after resource type") - } - return nil -} - -func (c *cmd) Synopsis() string { - return synopsis -} - -func (c *cmd) Help() string { - return flags.Usage(c.help, nil) -} - -const synopsis = "Lists all resources by name prefix" -const help = ` -Usage: consul resource list [type] -partition= -namespace= -peer= -or -consul resource list -f [path/to/file.hcl] - -Lists all the resources specified by the type under the given partition, namespace and peer -and outputs in JSON format. - -Example: - -$ consul resource list catalog.v2beta1.Service -p=card -partition=billing -namespace=payments -peer=eu - -$ consul resource list -f=demo.hcl -p=card - -Sample demo.hcl: - -ID { - Type = gvk("group.version.kind") - Tenancy { - Partition = "default" - Namespace = "default" - PeerName = "local" - } - } -` diff --git a/command/resource/list-grpc/list_test.go b/command/resource/list-grpc/list_test.go deleted file mode 100644 index b52f17a6bb..0000000000 --- a/command/resource/list-grpc/list_test.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package list - -import ( - "errors" - "fmt" - "testing" - - "github.com/mitchellh/cli" - - "github.com/hashicorp/consul/agent" - "github.com/hashicorp/consul/sdk/freeport" - "github.com/hashicorp/consul/testrpc" - - "github.com/stretchr/testify/require" - - "github.com/hashicorp/consul/command/resource/apply-grpc" -) - -func TestResourceListCommand(t *testing.T) { - if testing.Short() { - t.Skip("too slow for testing.Short") - } - - t.Parallel() - availablePort := freeport.GetOne(t) - a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) - testrpc.WaitForTestAgent(t, a.RPC, "dc1") - t.Cleanup(func() { - a.Shutdown() - }) - - applyCli := cli.NewMockUi() - - applyCmd := apply.New(applyCli) - code := applyCmd.Run([]string{ - "-f=../testdata/demo.hcl", - fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), - "-token=root", - }) - require.Equal(t, 0, code) - require.Empty(t, applyCli.ErrorWriter.String()) - require.Contains(t, applyCli.OutputWriter.String(), "demo.v2.Artist 'korn' created.") - - cases := []struct { - name string - output string - extraArgs []string - }{ - { - name: "sample output", - output: "\"name\": \"korn\"", - extraArgs: []string{ - "demo.v2.Artist", - "-partition=default", - "-namespace=default", - "-peer=local", - }, - }, - { - name: "sample output with name prefix", - output: "\"name\": \"korn\"", - extraArgs: []string{ - "demo.v2.Artist", - "-p=korn", - "-partition=default", - "-namespace=default", - "-peer=local", - }, - }, - { - name: "file input", - output: "\"name\": \"korn\"", - extraArgs: []string{ - "-f=../testdata/demo.hcl", - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - ui := cli.NewMockUi() - c := New(ui) - - args := []string{ - fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), - "-token=root", - } - - args = append(args, tc.extraArgs...) - - actualCode := c.Run(args) - require.Equal(t, 0, actualCode) - require.Empty(t, ui.ErrorWriter.String()) - require.Contains(t, ui.OutputWriter.String(), tc.output) - }) - } -} - -func TestResourceListInvalidArgs(t *testing.T) { - t.Parallel() - - availablePort := freeport.GetOne(t) - a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) - testrpc.WaitForTestAgent(t, a.RPC, "dc1") - t.Cleanup(func() { - a.Shutdown() - }) - - type tc struct { - args []string - expectedCode int - expectedErr error - } - - cases := map[string]tc{ - "nil args": { - args: nil, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must include resource type or flag arguments"), - }, - "minimum args required": { - args: []string{}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must include resource type argument"), - }, - "no file path": { - args: []string{ - "-f", - }, - expectedCode: 1, - expectedErr: errors.New("Failed to parse args: flag needs an argument: -f"), - }, - "file not found": { - args: []string{ - "-f=../testdata/test.hcl", - }, - expectedCode: 1, - expectedErr: errors.New("Failed to load data: Failed to read file: open ../testdata/test.hcl: no such file or directory"), - }, - "file parsing failure": { - args: []string{ - "-f=../testdata/invalid_type.hcl", - }, - expectedCode: 1, - expectedErr: errors.New("Failed to decode resource from input file"), - }, - "file argument with resource type": { - args: []string{ - "demo.v2.Artist", - "-partition=default", - "-namespace=default", - "-peer=local", - fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), - "-token=root", - "-f=demo.hcl", - }, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: File argument is not needed when resource information is provided with the command"), - }, - "resource type invalid": { - args: []string{ - "test", - "-partition=default", - "-namespace=default", - "-peer=local", - }, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: The shorthand name does not map to any existing resource type"), - }, - "resource name is provided": { - args: []string{ - "demo.v2.Artist", - "test", - "-namespace=default", - "-peer=local", - "-partition=default", - }, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must include flag arguments after resource type"), - }, - } - - for desc, tc := range cases { - t.Run(desc, func(t *testing.T) { - ui := cli.NewMockUi() - c := New(ui) - - code := c.Run(tc.args) - - require.Equal(t, tc.expectedCode, code) - require.Contains(t, ui.ErrorWriter.String(), tc.expectedErr.Error()) - }) - } -} diff --git a/command/resource/list/list.go b/command/resource/list/list.go index 55e8b503fb..9d07d9575a 100644 --- a/command/resource/list/list.go +++ b/command/resource/list/list.go @@ -12,10 +12,10 @@ import ( "github.com/mitchellh/cli" - "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/resource" "github.com/hashicorp/consul/command/resource/client" + "github.com/hashicorp/consul/proto-public/pbresource" ) func New(ui cli.Ui) *cmd { @@ -25,73 +25,75 @@ func New(ui cli.Ui) *cmd { } type cmd struct { - UI cli.Ui - flags *flag.FlagSet - http *flags.HTTPFlags - help string + UI cli.Ui + flags *flag.FlagSet + grpcFlags *client.GRPCFlags + resourceFlags *client.ResourceFlags + help string filePath string + prefix string } func (c *cmd) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) - c.http = &flags.HTTPFlags{} - c.flags.StringVar(&c.filePath, "f", "", "File path with resource definition") - flags.Merge(c.flags, c.http.ClientFlags()) - flags.Merge(c.flags, c.http.ServerFlags()) - flags.Merge(c.flags, c.http.MultiTenancyFlags()) - // TODO(peering/v2) add back ability to query peers - // flags.Merge(c.flags, c.http.AddPeerName()) - c.help = flags.Usage(help, c.flags) + c.flags.StringVar(&c.filePath, "f", "", + "File path with resource definition") + c.flags.StringVar(&c.prefix, "p", "", + "Name prefix for listing resources if you need ambiguous match") + + c.grpcFlags = &client.GRPCFlags{} + c.resourceFlags = &client.ResourceFlags{} + client.MergeFlags(c.flags, c.grpcFlags.ClientFlags()) + client.MergeFlags(c.flags, c.resourceFlags.ResourceFlags()) + c.help = client.Usage(help, c.flags) } func (c *cmd) Run(args []string) int { - var gvk *resource.GVK - var opts *client.QueryOptions + var resourceType *pbresource.Type + var resourceTenancy *pbresource.Tenancy if err := c.flags.Parse(args); err != nil { if !errors.Is(err, flag.ErrHelp) { c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) return 1 } + c.UI.Error(fmt.Sprintf("Failed to run list command: %v", err)) + return 1 } + // collect resource type, name and tenancy if c.flags.Lookup("f").Value.String() != "" { - if c.filePath != "" { - parsedResource, err := resource.ParseResourceFromFile(c.filePath) - if err != nil { - c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) - return 1 - } - - if parsedResource == nil { - c.UI.Error("Unable to parse the file argument") - return 1 - } - - gvk = &resource.GVK{ - Group: parsedResource.Id.Type.GetGroup(), - Version: parsedResource.Id.Type.GetGroupVersion(), - Kind: parsedResource.Id.Type.GetKind(), - } - opts = &client.QueryOptions{ - Namespace: parsedResource.Id.Tenancy.GetNamespace(), - Partition: parsedResource.Id.Tenancy.GetPartition(), - Token: c.http.Token(), - RequireConsistent: !c.http.Stale(), - } - } else { + if c.filePath == "" { c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) return 1 } - } else { - var err error - // extract resource type - gvk, err = getResourceType(c.flags.Args()) + parsedResource, err := resource.ParseResourceFromFile(c.filePath) if err != nil { - c.UI.Error(fmt.Sprintf("Incorrect argument format: %v", err)) + c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) return 1 } + + if parsedResource == nil { + c.UI.Error("Unable to parse the file argument") + return 1 + } + + resourceType = parsedResource.Id.Type + resourceTenancy = parsedResource.Id.Tenancy + } else { + var err error + args := c.flags.Args() + if err = validateArgs(args); err != nil { + c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) + return 1 + } + resourceType, err = resource.InferTypeFromResourceType(args[0]) + if err != nil { + c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) + return 1 + } + // skip resource type to parse remaining args inputArgs := c.flags.Args()[1:] err = resource.ParseInputParams(inputArgs, c.flags) @@ -103,33 +105,35 @@ func (c *cmd) Run(args []string) int { c.UI.Error("Incorrect argument format: File argument is not needed when resource information is provided with the command") return 1 } - - opts = &client.QueryOptions{ - Namespace: c.http.Namespace(), - Partition: c.http.Partition(), - Token: c.http.Token(), - RequireConsistent: !c.http.Stale(), + resourceTenancy = &pbresource.Tenancy{ + Partition: c.resourceFlags.Partition(), + Namespace: c.resourceFlags.Namespace(), } } - config := api.DefaultConfig() - - c.http.MergeOntoConfig(config) - resourceClient, err := client.NewClient(config) + // initialize client + config, err := client.LoadGRPCConfig(nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Error loading config: %s", err)) + return 1 + } + c.grpcFlags.MergeFlagsIntoGRPCConfig(config) + resourceClient, err := client.NewGRPCClient(config) if err != nil { c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) return 1 } - res := resource.Resource{C: resourceClient} - - entry, err := res.List(gvk, opts) + // list resource + res := resource.ResourceGRPC{C: resourceClient} + entry, err := res.List(resourceType, resourceTenancy, c.prefix, c.resourceFlags.Stale()) if err != nil { - c.UI.Error(fmt.Sprintf("Error reading resources for type %s: %v", gvk, err)) + c.UI.Error(fmt.Sprintf("Error listing resource %s/%s: %v", resourceType, c.prefix, err)) return 1 } - b, err := json.MarshalIndent(entry, "", " ") + // display response + b, err := json.MarshalIndent(entry, "", resource.JSON_INDENT) if err != nil { c.UI.Error("Failed to encode output data") return 1 @@ -139,26 +143,17 @@ func (c *cmd) Run(args []string) int { return 0 } -func getResourceType(args []string) (gvk *resource.GVK, e error) { +func validateArgs(args []string) error { + if args == nil { + return fmt.Errorf("Must include resource type or flag arguments") + } if len(args) < 1 { - return nil, fmt.Errorf("Must include resource type argument") + return fmt.Errorf("Must include resource type argument") } - // it should not have resource name if len(args) > 1 && !strings.HasPrefix(args[1], "-") { - return nil, fmt.Errorf("Must include flag arguments after resource type") + return fmt.Errorf("Must include flag arguments after resource type") } - - s := strings.Split(args[0], ".") - if len(s) < 3 { - return nil, fmt.Errorf("Must include resource type argument in group.version.kind format") - } - gvk = &resource.GVK{ - Group: s[0], - Version: s[1], - Kind: s[2], - } - - return + return nil } func (c *cmd) Synopsis() string { @@ -169,29 +164,28 @@ func (c *cmd) Help() string { return flags.Usage(c.help, nil) } -const synopsis = "Reads all resources by type" +const synopsis = "Lists all resources by name prefix" const help = ` Usage: consul resource list [type] -partition= -namespace= or consul resource list -f [path/to/file.hcl] -Lists all the resources specified by the type under the given partition, namespace and peer +Lists all the resources specified by the type under the given partition, namespace and outputs in JSON format. Example: -$ consul resource list catalog.v2beta1.Service card-processor -partition=billing -namespace=payments +$ consul resource list catalog.v2beta1.Service -p=card -partition=billing -namespace=payments -$ consul resource list -f=demo.hcl +$ consul resource list -f=demo.hcl -p=card Sample demo.hcl: ID { Type = gvk("group.version.kind") - Name = "resource-name" Tenancy { - Namespace = "default" - Partition = "default" + Partition = "default" + Namespace = "default" } } ` diff --git a/command/resource/list/list_test.go b/command/resource/list/list_test.go index 92afe4a6dc..a3df6c7524 100644 --- a/command/resource/list/list_test.go +++ b/command/resource/list/list_test.go @@ -5,11 +5,13 @@ package list import ( "errors" + "fmt" "testing" "github.com/mitchellh/cli" "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/sdk/freeport" "github.com/hashicorp/consul/testrpc" "github.com/stretchr/testify/require" @@ -23,16 +25,19 @@ func TestResourceListCommand(t *testing.T) { } t.Parallel() - a := agent.NewTestAgent(t, ``) - defer a.Shutdown() + availablePort := freeport.GetOne(t) + a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) testrpc.WaitForTestAgent(t, a.RPC, "dc1") + t.Cleanup(func() { + a.Shutdown() + }) applyCli := cli.NewMockUi() applyCmd := apply.New(applyCli) code := applyCmd.Run([]string{ "-f=../testdata/demo.hcl", - "-http-addr=" + a.HTTPAddr(), + fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), "-token=root", }) require.Equal(t, 0, code) @@ -48,9 +53,19 @@ func TestResourceListCommand(t *testing.T) { name: "sample output", output: "\"name\": \"korn\"", extraArgs: []string{ - "demo.v2.artist", - "-namespace=default", + "demo.v2.Artist", "-partition=default", + "-namespace=default", + }, + }, + { + name: "sample output with name prefix", + output: "\"name\": \"korn\"", + extraArgs: []string{ + "demo.v2.Artist", + "-p=korn", + "-partition=default", + "-namespace=default", }, }, { @@ -68,7 +83,7 @@ func TestResourceListCommand(t *testing.T) { c := New(ui) args := []string{ - "-http-addr=" + a.HTTPAddr(), + fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), "-token=root", } @@ -85,9 +100,12 @@ func TestResourceListCommand(t *testing.T) { func TestResourceListInvalidArgs(t *testing.T) { t.Parallel() - a := agent.NewTestAgent(t, ``) - defer a.Shutdown() + availablePort := freeport.GetOne(t) + a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) testrpc.WaitForTestAgent(t, a.RPC, "dc1") + t.Cleanup(func() { + a.Shutdown() + }) type tc struct { args []string @@ -99,7 +117,7 @@ func TestResourceListInvalidArgs(t *testing.T) { "nil args": { args: nil, expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must include resource type argument"), + expectedErr: errors.New("Incorrect argument format: Must include resource type or flag arguments"), }, "minimum args required": { args: []string{}, @@ -129,10 +147,10 @@ func TestResourceListInvalidArgs(t *testing.T) { }, "file argument with resource type": { args: []string{ - "demo.v2.artist", - "-namespace=default", + "demo.v2.Artist", "-partition=default", - "-http-addr=" + a.HTTPAddr(), + "-namespace=default", + fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), "-token=root", "-f=demo.hcl", }, @@ -142,21 +160,21 @@ func TestResourceListInvalidArgs(t *testing.T) { "resource type invalid": { args: []string{ "test", - "-namespace=default", "-partition=default", + "-namespace=default", }, expectedCode: 1, - expectedErr: errors.New("Must include resource type argument in group.version.kind format"), + expectedErr: errors.New("Incorrect argument format: The shorthand name does not map to any existing resource type"), }, "resource name is provided": { args: []string{ - "demo.v2.artist", + "demo.v2.Artist", "test", "-namespace=default", "-partition=default", }, expectedCode: 1, - expectedErr: errors.New("Must include flag arguments after resource type"), + expectedErr: errors.New("Incorrect argument format: Must include flag arguments after resource type"), }, } diff --git a/command/resource/read-grpc/read.go b/command/resource/read-grpc/read.go deleted file mode 100644 index c681074ba7..0000000000 --- a/command/resource/read-grpc/read.go +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package read - -import ( - "encoding/json" - "errors" - "flag" - "fmt" - - "github.com/mitchellh/cli" - - "github.com/hashicorp/consul/command/flags" - "github.com/hashicorp/consul/command/resource" - "github.com/hashicorp/consul/command/resource/client" - "github.com/hashicorp/consul/proto-public/pbresource" -) - -func New(ui cli.Ui) *cmd { - c := &cmd{UI: ui} - c.init() - return c -} - -type cmd struct { - UI cli.Ui - flags *flag.FlagSet - grpcFlags *client.GRPCFlags - resourceFlags *client.ResourceFlags - help string - - filePath string -} - -func (c *cmd) init() { - c.flags = flag.NewFlagSet("", flag.ContinueOnError) - c.flags.StringVar(&c.filePath, "f", "", - "File path with resource definition") - - c.grpcFlags = &client.GRPCFlags{} - c.resourceFlags = &client.ResourceFlags{} - client.MergeFlags(c.flags, c.grpcFlags.ClientFlags()) - client.MergeFlags(c.flags, c.resourceFlags.ResourceFlags()) - c.help = client.Usage(help, c.flags) -} - -func (c *cmd) Run(args []string) int { - var resourceType *pbresource.Type - var resourceTenancy *pbresource.Tenancy - var resourceName string - - if err := c.flags.Parse(args); err != nil { - if !errors.Is(err, flag.ErrHelp) { - c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) - return 1 - } - c.UI.Error(fmt.Sprintf("Failed to run read command: %v", err)) - return 1 - } - - // collect resource type, name and tenancy - if c.flags.Lookup("f").Value.String() != "" { - if c.filePath == "" { - c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) - return 1 - } - parsedResource, err := resource.ParseResourceFromFile(c.filePath) - if err != nil { - c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) - return 1 - } - - if parsedResource == nil { - c.UI.Error("The parsed resource is nil") - return 1 - } - - resourceType = parsedResource.Id.Type - resourceTenancy = parsedResource.Id.Tenancy - resourceName = parsedResource.Id.Name - } else { - var err error - resourceType, resourceName, err = resource.GetTypeAndResourceName(args) - if err != nil { - c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) - return 1 - } - - inputArgs := args[2:] - err = resource.ParseInputParams(inputArgs, c.flags) - if err != nil { - c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err)) - return 1 - } - if c.filePath != "" { - c.UI.Error("Incorrect argument format: File argument is not needed when resource information is provided with the command") - return 1 - } - resourceTenancy = &pbresource.Tenancy{ - Partition: c.resourceFlags.Partition(), - Namespace: c.resourceFlags.Namespace(), - } - } - - // initialize client - config, err := client.LoadGRPCConfig(nil) - if err != nil { - c.UI.Error(fmt.Sprintf("Error loading config: %s", err)) - return 1 - } - c.grpcFlags.MergeFlagsIntoGRPCConfig(config) - resourceClient, err := client.NewGRPCClient(config) - if err != nil { - c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) - return 1 - } - - // read resource - res := resource.ResourceGRPC{C: resourceClient} - entry, err := res.Read(resourceType, resourceTenancy, resourceName, c.resourceFlags.Stale()) - if err != nil { - c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", resourceType, resourceName, err)) - return 1 - } - - // display response - b, err := json.MarshalIndent(entry, "", resource.JSON_INDENT) - if err != nil { - c.UI.Error("Failed to encode output data") - return 1 - } - - c.UI.Info(string(b)) - return 0 -} - -func (c *cmd) Synopsis() string { - return synopsis -} - -func (c *cmd) Help() string { - return flags.Usage(c.help, nil) -} - -const synopsis = "Read resource information" -const help = ` -Usage: You have two options to read the resource specified by the given -type, name, partition, namespace and peer and outputs its JSON representation. - -consul resource read [type] [name] -partition= -namespace= -peer= -consul resource read -f [resource_file_path] - -But you could only use one of the approaches. - -Example: - -$ consul resource read catalog.v2beta1.Service card-processor -partition=billing -namespace=payments -peer=eu -$ consul resource read -f resource.hcl - -In resource.hcl, it could be: -ID { - Type = gvk("catalog.v2beta1.Service") - Name = "card-processor" - Tenancy { - Partition = "billing" - Namespace = "payments" - PeerName = "eu" - } -} -` diff --git a/command/resource/read-grpc/read_test.go b/command/resource/read-grpc/read_test.go deleted file mode 100644 index 4748f33d9e..0000000000 --- a/command/resource/read-grpc/read_test.go +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 -package read - -import ( - "errors" - "fmt" - "testing" - - "github.com/mitchellh/cli" - "github.com/stretchr/testify/require" - - "github.com/hashicorp/consul/agent" - "github.com/hashicorp/consul/command/resource/apply-grpc" - "github.com/hashicorp/consul/sdk/freeport" - "github.com/hashicorp/consul/testrpc" -) - -func TestResourceReadInvalidArgs(t *testing.T) { - t.Parallel() - - type tc struct { - args []string - expectedCode int - expectedErr error - } - - cases := map[string]tc{ - "nil args": { - args: nil, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must specify two arguments: resource type and resource name"), - }, - "empty args": { - args: []string{}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must specify two arguments: resource type and resource name"), - }, - "missing file path": { - args: []string{"-f"}, - expectedCode: 1, - expectedErr: errors.New("Failed to parse args: flag needs an argument: -f"), - }, - "file not found": { - args: []string{"-f=../testdata/test.hcl"}, - expectedCode: 1, - expectedErr: errors.New("Failed to load data: Failed to read file: open ../testdata/test.hcl: no such file or directory"), - }, - "provide type and name": { - args: []string{"a.b.c"}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must specify two arguments: resource type and resource name"), - }, - "provide type and name with -f": { - args: []string{"a.b.c", "name", "-f", "test.hcl"}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: File argument is not needed when resource information is provided with the command"), - }, - "provide type and name with -f and other flags": { - args: []string{"a.b.c", "name", "-f", "test.hcl", "-namespace", "default"}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: File argument is not needed when resource information is provided with the command"), - }, - "does not provide resource name after type": { - args: []string{"a.b.c", "-namespace", "default"}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must provide resource name right after type"), - }, - "invalid resource type format": { - args: []string{"a.", "name", "-namespace", "default"}, - expectedCode: 1, - expectedErr: errors.New("Incorrect argument format: Must provide resource type argument with either in group.version.kind format or its shorthand name"), - }, - } - - for desc, tc := range cases { - t.Run(desc, func(t *testing.T) { - ui := cli.NewMockUi() - c := New(ui) - - code := c.Run(tc.args) - - require.Equal(t, tc.expectedCode, code) - require.Contains(t, ui.ErrorWriter.String(), tc.expectedErr.Error()) - }) - } -} - -func createResource(t *testing.T, port int) { - applyUi := cli.NewMockUi() - applyCmd := apply.New(applyUi) - - args := []string{ - fmt.Sprintf("-grpc-addr=127.0.0.1:%d", port), - "-token=root", - } - - args = append(args, []string{"-f=../testdata/demo.hcl"}...) - - code := applyCmd.Run(args) - require.Equal(t, 0, code) - require.Empty(t, applyUi.ErrorWriter.String()) -} - -func TestResourceRead(t *testing.T) { - if testing.Short() { - t.Skip("too slow for testing.Short") - } - - t.Parallel() - - availablePort := freeport.GetOne(t) - a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) - testrpc.WaitForTestAgent(t, a.RPC, "dc1") - t.Cleanup(func() { - a.Shutdown() - }) - - defaultCmdArgs := []string{ - fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), - "-token=root", - } - - createResource(t, availablePort) - cases := []struct { - name string - args []string - expectedCode int - errMsg string - }{ - { - name: "read resource in hcl format", - args: []string{"-f=../testdata/demo.hcl"}, - expectedCode: 0, - errMsg: "", - }, - { - name: "read resource in command line format", - args: []string{"demo.v2.Artist", "korn", "-partition=default", "-namespace=default", "-peer=local"}, - expectedCode: 0, - errMsg: "", - }, - { - name: "read resource that doesn't exist", - args: []string{"demo.v2.Artist", "fake-korn", "-partition=default", "-namespace=default", "-peer=local"}, - expectedCode: 1, - errMsg: "error reading resource: rpc error: code = NotFound desc = resource not found\n", - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - ui := cli.NewMockUi() - c := New(ui) - cliArgs := append(tc.args, defaultCmdArgs...) - code := c.Run(cliArgs) - require.Contains(t, ui.ErrorWriter.String(), tc.errMsg) - require.Equal(t, tc.expectedCode, code) - }) - } -} diff --git a/command/resource/read/read.go b/command/resource/read/read.go index 32a39cd1fd..0eb7a945bf 100644 --- a/command/resource/read/read.go +++ b/command/resource/read/read.go @@ -11,7 +11,6 @@ import ( "github.com/mitchellh/cli" - "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/resource" "github.com/hashicorp/consul/command/resource/client" @@ -25,76 +24,64 @@ func New(ui cli.Ui) *cmd { } type cmd struct { - UI cli.Ui - flags *flag.FlagSet - http *flags.HTTPFlags - help string + UI cli.Ui + flags *flag.FlagSet + grpcFlags *client.GRPCFlags + resourceFlags *client.ResourceFlags + help string filePath string } func (c *cmd) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) - c.http = &flags.HTTPFlags{} - c.flags.StringVar(&c.filePath, "f", "", "File path with resource definition") - flags.Merge(c.flags, c.http.ClientFlags()) - flags.Merge(c.flags, c.http.ServerFlags()) - flags.Merge(c.flags, c.http.MultiTenancyFlags()) - // TODO(peering/v2) add back ability to query peers - // flags.Merge(c.flags, c.http.AddPeerName()) - c.help = flags.Usage(help, c.flags) + c.flags.StringVar(&c.filePath, "f", "", + "File path with resource definition") + + c.grpcFlags = &client.GRPCFlags{} + c.resourceFlags = &client.ResourceFlags{} + client.MergeFlags(c.flags, c.grpcFlags.ClientFlags()) + client.MergeFlags(c.flags, c.resourceFlags.ResourceFlags()) + c.help = client.Usage(help, c.flags) } func (c *cmd) Run(args []string) int { - var gvk *resource.GVK + var resourceType *pbresource.Type + var resourceTenancy *pbresource.Tenancy var resourceName string - var opts *client.QueryOptions if err := c.flags.Parse(args); err != nil { if !errors.Is(err, flag.ErrHelp) { c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) return 1 } + c.UI.Error(fmt.Sprintf("Failed to run read command: %v", err)) + return 1 } + // collect resource type, name and tenancy if c.flags.Lookup("f").Value.String() != "" { - if c.filePath != "" { - parsedResource, err := resource.ParseResourceFromFile(c.filePath) - if err != nil { - c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) - return 1 - } - - if parsedResource == nil { - c.UI.Error("Unable to parse the file argument") - return 1 - } - - gvk = &resource.GVK{ - Group: parsedResource.Id.Type.GetGroup(), - Version: parsedResource.Id.Type.GetGroupVersion(), - Kind: parsedResource.Id.Type.GetKind(), - } - resourceName = parsedResource.Id.GetName() - opts = &client.QueryOptions{ - Namespace: parsedResource.Id.Tenancy.GetNamespace(), - Partition: parsedResource.Id.Tenancy.GetPartition(), - Token: c.http.Token(), - RequireConsistent: !c.http.Stale(), - } - } else { + if c.filePath == "" { c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) return 1 } + parsedResource, err := resource.ParseResourceFromFile(c.filePath) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) + return 1 + } + + if parsedResource == nil { + c.UI.Error("The parsed resource is nil") + return 1 + } + + resourceType = parsedResource.Id.Type + resourceTenancy = parsedResource.Id.Tenancy + resourceName = parsedResource.Id.Name } else { var err error - var resourceType *pbresource.Type resourceType, resourceName, err = resource.GetTypeAndResourceName(args) - gvk = &resource.GVK{ - Group: resourceType.GetGroup(), - Version: resourceType.GetGroupVersion(), - Kind: resourceType.GetKind(), - } if err != nil { c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) return 1 @@ -110,32 +97,35 @@ func (c *cmd) Run(args []string) int { c.UI.Error("Incorrect argument format: File argument is not needed when resource information is provided with the command") return 1 } - opts = &client.QueryOptions{ - Namespace: c.http.Namespace(), - Partition: c.http.Partition(), - Token: c.http.Token(), - RequireConsistent: !c.http.Stale(), + resourceTenancy = &pbresource.Tenancy{ + Partition: c.resourceFlags.Partition(), + Namespace: c.resourceFlags.Namespace(), } } - config := api.DefaultConfig() - - c.http.MergeOntoConfig(config) - resourceClient, err := client.NewClient(config) + // initialize client + config, err := client.LoadGRPCConfig(nil) if err != nil { - c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) + c.UI.Error(fmt.Sprintf("Error loading config: %s", err)) + return 1 + } + c.grpcFlags.MergeFlagsIntoGRPCConfig(config) + resourceClient, err := client.NewGRPCClient(config) + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) return 1 } - res := resource.Resource{C: resourceClient} - - entry, err := res.Read(gvk, resourceName, opts) + // read resource + res := resource.ResourceGRPC{C: resourceClient} + entry, err := res.Read(resourceType, resourceTenancy, resourceName, c.resourceFlags.Stale()) if err != nil { - c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", gvk, resourceName, err)) + c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", resourceType, resourceName, err)) return 1 } - b, err := json.MarshalIndent(entry, "", " ") + // display response + b, err := json.MarshalIndent(entry, "", resource.JSON_INDENT) if err != nil { c.UI.Error("Failed to encode output data") return 1 @@ -156,7 +146,7 @@ func (c *cmd) Help() string { const synopsis = "Read resource information" const help = ` Usage: You have two options to read the resource specified by the given -type, name, partition, namespace and peer and outputs its JSON representation. +type, name, partition, namespace and outputs its JSON representation. consul resource read [type] [name] -partition= -namespace= consul resource read -f [resource_file_path] @@ -170,11 +160,12 @@ $ consul resource read -f resource.hcl In resource.hcl, it could be: ID { - Type = gvk("catalog.v2beta1.Service") - Name = "card-processor" - Tenancy { - Namespace = "payments" - Partition = "billing" - } +<<<<<<< HEAD + Type = gvk("catalog.v2beta1.Service") + Name = "card-processor" + Tenancy { + Partition = "billing" + Namespace = "payments" + } } ` diff --git a/command/resource/read/read_test.go b/command/resource/read/read_test.go index 1a4de5703e..2df02baa39 100644 --- a/command/resource/read/read_test.go +++ b/command/resource/read/read_test.go @@ -4,6 +4,7 @@ package read import ( "errors" + "fmt" "testing" "github.com/mitchellh/cli" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/command/resource/apply" + "github.com/hashicorp/consul/sdk/freeport" "github.com/hashicorp/consul/testrpc" ) @@ -84,12 +86,12 @@ func TestResourceReadInvalidArgs(t *testing.T) { } } -func createResource(t *testing.T, a *agent.TestAgent) { +func createResource(t *testing.T, port int) { applyUi := cli.NewMockUi() applyCmd := apply.New(applyUi) args := []string{ - "-http-addr=" + a.HTTPAddr(), + fmt.Sprintf("-grpc-addr=127.0.0.1:%d", port), "-token=root", } @@ -107,16 +109,19 @@ func TestResourceRead(t *testing.T) { t.Parallel() - a := agent.NewTestAgent(t, ``) - defer a.Shutdown() + availablePort := freeport.GetOne(t) + a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort)) testrpc.WaitForTestAgent(t, a.RPC, "dc1") + t.Cleanup(func() { + a.Shutdown() + }) defaultCmdArgs := []string{ - "-http-addr=" + a.HTTPAddr(), + fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort), "-token=root", } - createResource(t, a) + createResource(t, availablePort) cases := []struct { name string args []string @@ -139,7 +144,7 @@ func TestResourceRead(t *testing.T) { name: "read resource that doesn't exist", args: []string{"demo.v2.Artist", "fake-korn", "-partition=default", "-namespace=default"}, expectedCode: 1, - errMsg: "Error reading resource demo.v2.Artist/fake-korn: Unexpected response code: 404 (rpc error: code = NotFound desc = resource not found)\n", + errMsg: "error reading resource: rpc error: code = NotFound desc = resource not found\n", }, } @@ -149,7 +154,7 @@ func TestResourceRead(t *testing.T) { c := New(ui) cliArgs := append(tc.args, defaultCmdArgs...) code := c.Run(cliArgs) - require.Equal(t, tc.errMsg, ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), tc.errMsg) require.Equal(t, tc.expectedCode, code) }) }