From aea9135f48fdb629d9589633638b3f06745210c0 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Fri, 11 Aug 2023 09:10:34 -0700 Subject: [PATCH 01/13] save read work --- api/resource.go | 43 +++++++++ command/flags/http.go | 11 +++ command/registry.go | 4 + command/resource/read/resource_read.go | 125 +++++++++++++++++++++++++ command/resource/resource.go | 47 ++++++++++ 5 files changed, 230 insertions(+) create mode 100644 api/resource.go create mode 100644 command/resource/read/resource_read.go create mode 100644 command/resource/resource.go diff --git a/api/resource.go b/api/resource.go new file mode 100644 index 00000000000..a1736e062d3 --- /dev/null +++ b/api/resource.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package api + +import ( + "fmt" +) + +type Resource struct { + c *Client +} + +type GVK struct { + Group string + Version string + Kind string +} + +// Config returns a handle to the Config endpoints +func (c *Client) Resource() *Resource { + return &Resource{c} +} + +func (resource *Resource) Read(gvk *GVK, resourceName string, q *QueryOptions) ([]byte, error) { + r := resource.c.newRequest("GET", 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 closeResponseBody(resp) + if err := requireOK(resp); err != nil { + return nil, err + } + + var out []byte + if err := decodeBody(resp, &out); err != nil { + return nil, err + } + + return out, nil +} diff --git a/command/flags/http.go b/command/flags/http.go index 16157adc8a1..009c26629c1 100644 --- a/command/flags/http.go +++ b/command/flags/http.go @@ -29,6 +29,7 @@ type HTTPFlags struct { // multi-tenancy flags namespace StringValue partition StringValue + peer StringValue } func (f *HTTPFlags) ClientFlags() *flag.FlagSet { @@ -109,6 +110,10 @@ func (f *HTTPFlags) Partition() string { return f.partition.String() } +func (f *HTTPFlags) PeerName() string { + return f.peer.String() +} + func (f *HTTPFlags) Stale() bool { if f.stale.v == nil { return false @@ -174,3 +179,9 @@ func (f *HTTPFlags) AddPartitionFlag(fs *flag.FlagSet) { "from the request's ACL token, or will default to the `default` admin partition. "+ "Admin Partitions are a Consul Enterprise feature.") } + +func (f *HTTPFlags) AddPeerName() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.Var(&f.peer, "peer", "Specifies the name of peer to query. By default, it is `local`.") + return fs +} diff --git a/command/registry.go b/command/registry.go index f01e3a10804..55bfb1ad598 100644 --- a/command/registry.go +++ b/command/registry.go @@ -108,6 +108,8 @@ import ( peerlist "github.com/hashicorp/consul/command/peering/list" peerread "github.com/hashicorp/consul/command/peering/read" "github.com/hashicorp/consul/command/reload" + "github.com/hashicorp/consul/command/resource" + resourceread "github.com/hashicorp/consul/command/resource/read" "github.com/hashicorp/consul/command/rtt" "github.com/hashicorp/consul/command/services" svcsderegister "github.com/hashicorp/consul/command/services/deregister" @@ -238,6 +240,8 @@ func RegisteredCommands(ui cli.Ui) map[string]mcli.CommandFactory { entry{"peering list", func(ui cli.Ui) (cli.Command, error) { return peerlist.New(ui), nil }}, 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{"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/read/resource_read.go b/command/resource/read/resource_read.go new file mode 100644 index 00000000000..ae4d86e19a8 --- /dev/null +++ b/command/resource/read/resource_read.go @@ -0,0 +1,125 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package read + +import ( + "encoding/json" + "flag" + "fmt" + "strings" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + consistent bool +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + c.flags.BoolVar(&c.consistent, "consistent", false, "The reading mode.") + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.ServerFlags()) + flags.Merge(c.flags, c.http.MultiTenancyFlags()) + flags.Merge(c.flags, c.http.AddPeerName()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + if err == flag.ErrHelp { + return 0 + } + c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) + return 1 + } + + args = c.flags.Args() + gvk, resourceName, e := parseArgs(args) + if e != nil { + c.UI.Error(fmt.Sprintf("Your argument format is incorrect: %s", e)) + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) + return 1 + } + + opts := &api.QueryOptions{ + Namespace: c.http.Namespace(), + Partition: c.http.Partition(), + Peer: c.http.PeerName(), + Token: c.http.Token(), + RequireConsistent: c.consistent, + } + + entry, err := client.Resource().Read(gvk, resourceName, opts) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", gvk, resourceName, err)) + return 1 + } + + b, err := json.MarshalIndent(entry, "", " ") + if err != nil { + c.UI.Error("Failed to encode output data") + return 1 + } + + c.UI.Info(string(b)) + return 0 +} + +func parseArgs(args []string) (gvk *api.GVK, resourceName string, e error) { + fmt.Println(args) + if len(args) < 2 { + return nil, "", fmt.Errorf("Must specify two arguments: resource types and resource name") + } + + s := strings.Split(args[0], ".") + gvk = &api.GVK{ + Group: s[0], + Version: s[1], + Kind: s[2], + } + + resourceName = args[1] + return +} + +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: consul resource read [type] [name] -partition= -namespace= -peer= -consistent= -json + +Reads the resource specified by the given type, name, partition, namespace, peer and reading mode +and outputs its JSON representation. + +Example: + +$ consul resource read catalog.v1alpha1.Service card-processor -partition=billing -namespace=payments -peer=eu +` diff --git a/command/resource/resource.go b/command/resource/resource.go new file mode 100644 index 00000000000..7ea93d7025e --- /dev/null +++ b/command/resource/resource.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/command/flags" +) + +func New() *cmd { + return &cmd{} +} + +type cmd struct{} + +func (c *cmd) Run(args []string) int { + return cli.RunResultHelp +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(help, nil) +} + +const synopsis = "Interact with Consul's resources" +const help = ` +Usage: consul resource [options] + +This command has subcommands for interacting with Consul's resources. +Here are some simple examples, and more detailed examples are available +in the subcommands or the documentation. + +Read a resource: + +$ consul resource read [type] [name] -partition= -namespace= -peer= -consistent= -json + +Run + +consul resource -h + +for help on that subcommand. +` From bfc790959a0af9c304f4ad7cc6137012a794fb40 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Mon, 14 Aug 2023 15:21:21 -0700 Subject: [PATCH 02/13] renaming --- command/resource/read/resource_read.go | 12 ++++------- internal/resource/http/http.go | 8 +++++-- internal/resource/http/http_test.go | 30 +++++++++++++------------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/command/resource/read/resource_read.go b/command/resource/read/resource_read.go index ae4d86e19a8..36e4f4a3082 100644 --- a/command/resource/read/resource_read.go +++ b/command/resource/read/resource_read.go @@ -26,13 +26,10 @@ type cmd struct { flags *flag.FlagSet http *flags.HTTPFlags help string - - consistent bool } func (c *cmd) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) - c.flags.BoolVar(&c.consistent, "consistent", false, "The reading mode.") c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -64,11 +61,10 @@ func (c *cmd) Run(args []string) int { } opts := &api.QueryOptions{ - Namespace: c.http.Namespace(), - Partition: c.http.Partition(), - Peer: c.http.PeerName(), - Token: c.http.Token(), - RequireConsistent: c.consistent, + Namespace: c.http.Namespace(), + Partition: c.http.Partition(), + Peer: c.http.PeerName(), + Token: c.http.Token(), } entry, err := client.Resource().Read(gvk, resourceName, opts) diff --git a/internal/resource/http/http.go b/internal/resource/http/http.go index 569f143e2fd..cce48f247f3 100644 --- a/internal/resource/http/http.go +++ b/internal/resource/http/http.go @@ -167,10 +167,14 @@ func (h *resourceHandler) handleDelete(w http.ResponseWriter, r *http.Request, c func parseParams(r *http.Request) (tenancy *pbresource.Tenancy, params map[string]string) { query := r.URL.Query() + namespace := query.Get("namespace") + if namespace == "" { + namespace = query.Get("ns") + } tenancy = &pbresource.Tenancy{ Partition: query.Get("partition"), - PeerName: query.Get("peer_name"), - Namespace: query.Get("namespace"), + PeerName: query.Get("peer"), + Namespace: namespace, } resourceName := path.Base(r.URL.Path) diff --git a/internal/resource/http/http_test.go b/internal/resource/http/http_test.go index 07c2b9dbc82..f47bf471271 100644 --- a/internal/resource/http/http_test.go +++ b/internal/resource/http/http_test.go @@ -54,7 +54,7 @@ func TestResourceHandler_InputValidation(t *testing.T) { testCases := []testCase{ { description: "missing resource name", - request: httptest.NewRequest("PUT", "/?partition=default&peer_name=local&namespace=default", strings.NewReader(` + request: httptest.NewRequest("PUT", "/?partition=default&peer=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -70,7 +70,7 @@ func TestResourceHandler_InputValidation(t *testing.T) { }, { description: "wrong schema", - request: httptest.NewRequest("PUT", "/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(` + request: httptest.NewRequest("PUT", "/keith-urban?partition=default&peer=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -86,7 +86,7 @@ func TestResourceHandler_InputValidation(t *testing.T) { }, { description: "no id", - request: httptest.NewRequest("DELETE", "/?partition=default&peer_name=local&namespace=default", strings.NewReader("")), + request: httptest.NewRequest("DELETE", "/?partition=default&peer=local&namespace=default", strings.NewReader("")), response: httptest.NewRecorder(), expectedResponseCode: http.StatusBadRequest, }, @@ -116,7 +116,7 @@ func TestResourceWriteHandler(t *testing.T) { t.Run("should be blocked if the token is not authorized", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(` + req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -137,7 +137,7 @@ func TestResourceWriteHandler(t *testing.T) { t.Run("should write to the resource backend", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(` + req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -177,7 +177,7 @@ func TestResourceWriteHandler(t *testing.T) { t.Run("should update the record with version parameter", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&version=1", strings.NewReader(` + req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default&version=1", strings.NewReader(` { "metadata": { "foo": "bar" @@ -202,7 +202,7 @@ func TestResourceWriteHandler(t *testing.T) { t.Run("should fail the update if the resource's version doesn't match the version of the existing resource", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&version=1", strings.NewReader(` + req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default&version=1", strings.NewReader(` { "metadata": { "foo": "bar" @@ -223,7 +223,7 @@ func TestResourceWriteHandler(t *testing.T) { t.Run("should write to the resource backend with owner", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v1/artist/keith-urban-v1?partition=default&peer_name=local&namespace=default", strings.NewReader(` + req := httptest.NewRequest("PUT", "/demo/v1/artist/keith-urban-v1?partition=default&peer=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -278,7 +278,7 @@ func TestResourceWriteHandler(t *testing.T) { func createResource(t *testing.T, artistHandler http.Handler) map[string]any { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(` + req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -319,7 +319,7 @@ func TestResourceReadHandler(t *testing.T) { t.Run("Read resource", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&consistent", nil) + req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default&consistent", nil) req.Header.Add("x-consul-token", testACLTokenArtistReadPolicy) @@ -334,7 +334,7 @@ func TestResourceReadHandler(t *testing.T) { t.Run("should not be found if resource not exist", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/demo/v2/artist/keith-not-exist?partition=default&peer_name=local&namespace=default&consistent", nil) + req := httptest.NewRequest("GET", "/demo/v2/artist/keith-not-exist?partition=default&peer=local&namespace=default&consistent", nil) req.Header.Add("x-consul-token", testACLTokenArtistReadPolicy) @@ -345,7 +345,7 @@ func TestResourceReadHandler(t *testing.T) { t.Run("should be blocked if the token is not authorized", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&consistent", nil) + req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default&consistent", nil) req.Header.Add("x-consul-token", fakeToken) @@ -373,7 +373,7 @@ func TestResourceDeleteHandler(t *testing.T) { createResource(t, handler) deleteRsp := httptest.NewRecorder() - deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader("")) + deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default", strings.NewReader("")) deletReq.Header.Add("x-consul-token", testACLTokenArtistReadPolicy) @@ -386,7 +386,7 @@ func TestResourceDeleteHandler(t *testing.T) { createResource(t, handler) deleteRsp := httptest.NewRecorder() - deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader("")) + deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default", strings.NewReader("")) deletReq.Header.Add("x-consul-token", testACLTokenArtistWritePolicy) @@ -412,7 +412,7 @@ func TestResourceDeleteHandler(t *testing.T) { createResource(t, handler) rsp := httptest.NewRecorder() - req := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&version=1", strings.NewReader("")) + req := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default&version=1", strings.NewReader("")) req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy) From 4ead439a5ecc032678b9f0696fb1141f7f17f509 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Tue, 22 Aug 2023 10:59:03 -0700 Subject: [PATCH 03/13] parse hcl and validate args --- api/resource.go | 4 +- command/resource/read/resource_read.go | 93 ++++++++++++++++++++------ 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/api/resource.go b/api/resource.go index a1736e062d3..bbddcbc3832 100644 --- a/api/resource.go +++ b/api/resource.go @@ -22,7 +22,7 @@ func (c *Client) Resource() *Resource { return &Resource{c} } -func (resource *Resource) Read(gvk *GVK, resourceName string, q *QueryOptions) ([]byte, error) { +func (resource *Resource) Read(gvk *GVK, resourceName string, q *QueryOptions) (map[string]interface{}, error) { r := resource.c.newRequest("GET", fmt.Sprintf("/api/%s/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind, resourceName)) r.setQueryOptions(q) _, resp, err := resource.c.doRequest(r) @@ -34,7 +34,7 @@ func (resource *Resource) Read(gvk *GVK, resourceName string, q *QueryOptions) ( return nil, err } - var out []byte + var out map[string]interface{} if err := decodeBody(resp, &out); err != nil { return nil, err } diff --git a/command/resource/read/resource_read.go b/command/resource/read/resource_read.go index 36e4f4a3082..ad22b468a0a 100644 --- a/command/resource/read/resource_read.go +++ b/command/resource/read/resource_read.go @@ -11,8 +11,11 @@ import ( "github.com/mitchellh/cli" + "github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/command/helpers" + "github.com/hashicorp/consul/internal/resourcehcl" ) func New(ui cli.Ui) *cmd { @@ -26,11 +29,14 @@ type cmd struct { flags *flag.FlagSet http *flags.HTTPFlags 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()) @@ -39,19 +45,72 @@ func (c *cmd) init() { } func (c *cmd) Run(args []string) int { - if err := c.flags.Parse(args); err != nil { - if err == flag.ErrHelp { - return 0 + var gvk *api.GVK + var resourceName string + var opts *api.QueryOptions + + if args[0] == "-f" { + if err := c.flags.Parse(args); err != nil { + if err == flag.ErrHelp { + return 0 + } + c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) + return 1 + } + if c.filePath != "" { + data, err := helpers.LoadDataSourceNoRaw(c.filePath, nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to load data: %v", err)) + return 1 + } + parsedResource, err := resourcehcl.Unmarshal([]byte(data), consul.NewTypeRegistry()) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) + return 1 + } + + gvk = &api.GVK{ + Group: parsedResource.Id.Type.GetGroup(), + Version: parsedResource.Id.Type.GetGroupVersion(), + Kind: parsedResource.Id.Type.GetKind(), + } + resourceName = parsedResource.Id.GetName() + opts = &api.QueryOptions{ + Namespace: parsedResource.Id.Tenancy.GetNamespace(), + Partition: parsedResource.Id.Tenancy.GetPartition(), + Peer: parsedResource.Id.Tenancy.GetPeerName(), + Token: c.http.Token(), + } + } else { + c.UI.Error("Please input file path") + return 1 + } + } else { + var err error + gvk, resourceName, err = getTypeAndResourceName(args) + if err != nil { + c.UI.Error(fmt.Sprintf("Your argument format is incorrect: %s", err)) + return 1 } - c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) - return 1 - } - args = c.flags.Args() - gvk, resourceName, e := parseArgs(args) - if e != nil { - c.UI.Error(fmt.Sprintf("Your argument format is incorrect: %s", e)) - return 1 + inputArgs := args[2:] + if err := c.flags.Parse(inputArgs); err != nil { + if err == flag.ErrHelp { + return 0 + } + c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) + return 1 + } + if c.filePath != "" { + c.UI.Error("You need to provide all information in the HCL file if provide its file path") + return 1 + } + opts = &api.QueryOptions{ + Namespace: c.http.Namespace(), + Partition: c.http.Partition(), + Peer: c.http.PeerName(), + Token: c.http.Token(), + } } client, err := c.http.APIClient() @@ -60,13 +119,6 @@ func (c *cmd) Run(args []string) int { return 1 } - opts := &api.QueryOptions{ - Namespace: c.http.Namespace(), - Partition: c.http.Partition(), - Peer: c.http.PeerName(), - Token: c.http.Token(), - } - entry, err := client.Resource().Read(gvk, resourceName, opts) if err != nil { c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", gvk, resourceName, err)) @@ -83,10 +135,9 @@ func (c *cmd) Run(args []string) int { return 0 } -func parseArgs(args []string) (gvk *api.GVK, resourceName string, e error) { - fmt.Println(args) +func getTypeAndResourceName(args []string) (gvk *api.GVK, resourceName string, e error) { if len(args) < 2 { - return nil, "", fmt.Errorf("Must specify two arguments: resource types and resource name") + return nil, "", fmt.Errorf("Must specify two arguments: resource type and resource name") } s := strings.Split(args[0], ".") From b2a62750b48b847a86e9479aa6e0c4478ddc0d80 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Tue, 22 Aug 2023 10:59:32 -0700 Subject: [PATCH 04/13] rename file --- command/resource/read/{resource_read.go => read.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename command/resource/read/{resource_read.go => read.go} (100%) diff --git a/command/resource/read/resource_read.go b/command/resource/read/read.go similarity index 100% rename from command/resource/read/resource_read.go rename to command/resource/read/read.go From b1a45afff921ebe02ac4dc241d82125f562fdf38 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Tue, 22 Aug 2023 12:04:14 -0700 Subject: [PATCH 05/13] add unit tests --- command/resource/read/read_test.go | 77 ++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 command/resource/read/read_test.go diff --git a/command/resource/read/read_test.go b/command/resource/read/read_test.go new file mode 100644 index 00000000000..0a5930910ea --- /dev/null +++ b/command/resource/read/read_test.go @@ -0,0 +1,77 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package read + +import ( + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" +) + +func TestResourceReadInvalidArgs(t *testing.T) { + t.Parallel() + + type tc struct { + args []string + expectedCode int + expectedErrMsg string + } + + cases := map[string]tc{ + "missing file path": { + args: []string{"-f"}, + expectedCode: 1, + expectedErrMsg: "Please input file path", + }, + "provide type and name": { + args: []string{"a.b.c"}, + expectedCode: 1, + expectedErrMsg: "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, + expectedErrMsg: "You need to provide all information in the HCL file if provide its file path", + }, + "provide type and name with -f and other flags": { + args: []string{"a.b.c", "name", "-f", "test.hcl", "-namespace", "default"}, + expectedCode: 1, + expectedErrMsg: "You need to provide all information in the HCL file if provide its file path", + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + ui := cli.NewMockUi() + c := New(ui) + + require.Equal(t, tc.expectedCode, c.Run(tc.args)) + require.NotEmpty(t, ui.ErrorWriter.String()) + }) + } +} + +func TestResourceRead(t *testing.T) { + // TODO: add read test after apply checked in + //if testing.Short() { + // t.Skip("too slow for testing.Short") + //} + // + //t.Parallel() + // + //a := agent.NewTestAgent(t, ``) + //defer a.Shutdown() + //client := a.Client() + // + //ui := cli.NewMockUi() + //c := New(ui) + + //_, _, err := client.Resource().Apply() + //require.NoError(t, err) + // + //args := []string{} + // + //code := c.Run(args) + //require.Equal(t, 0, code) +} From 2508d6281f40b861c01ad220c37ee5d0b3eba72d Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Tue, 22 Aug 2023 12:08:29 -0700 Subject: [PATCH 06/13] update license header --- api/resource.go | 2 +- command/resource/read/read.go | 6 +++--- command/resource/resource.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/resource.go b/api/resource.go index bbddcbc3832..c80fc3e4443 100644 --- a/api/resource.go +++ b/api/resource.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: BUSL-1.1 package api diff --git a/command/resource/read/read.go b/command/resource/read/read.go index ad22b468a0a..17f69e815ca 100644 --- a/command/resource/read/read.go +++ b/command/resource/read/read.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: BUSL-1.1 package read @@ -70,9 +70,9 @@ func (c *cmd) Run(args []string) int { } gvk = &api.GVK{ - Group: parsedResource.Id.Type.GetGroup(), + Group: parsedResource.Id.Type.GetGroup(), Version: parsedResource.Id.Type.GetGroupVersion(), - Kind: parsedResource.Id.Type.GetKind(), + Kind: parsedResource.Id.Type.GetKind(), } resourceName = parsedResource.Id.GetName() opts = &api.QueryOptions{ diff --git a/command/resource/resource.go b/command/resource/resource.go index 7ea93d7025e..cd9b8313dba 100644 --- a/command/resource/resource.go +++ b/command/resource/resource.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: BUSL-1.1 package resource From 5f16d90ffb8133e003d8d5ece1b72afc76e68c45 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Tue, 22 Aug 2023 12:17:14 -0700 Subject: [PATCH 07/13] add more test --- command/resource/read/read.go | 3 +++ command/resource/read/read_test.go | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/command/resource/read/read.go b/command/resource/read/read.go index 17f69e815ca..56a3213bc4c 100644 --- a/command/resource/read/read.go +++ b/command/resource/read/read.go @@ -139,6 +139,9 @@ func getTypeAndResourceName(args []string) (gvk *api.GVK, resourceName string, e if len(args) < 2 { return nil, "", fmt.Errorf("Must specify two arguments: resource type and resource name") } + if strings.HasPrefix(args[1], "-") { + return nil, "", fmt.Errorf("Must provide resource name right after type") + } s := strings.Split(args[0], ".") gvk = &api.GVK{ diff --git a/command/resource/read/read_test.go b/command/resource/read/read_test.go index 0a5930910ea..d04a715cc3e 100644 --- a/command/resource/read/read_test.go +++ b/command/resource/read/read_test.go @@ -39,6 +39,11 @@ func TestResourceReadInvalidArgs(t *testing.T) { expectedCode: 1, expectedErrMsg: "You need to provide all information in the HCL file if provide its file path", }, + "does not provide resource name after type": { + args: []string{"a.b.c", "-namespace", "default"}, + expectedCode: 1, + expectedErrMsg: "Must provide resource name right after type", + }, } for desc, tc := range cases { From abe49516c8b5a41a1909b3677f12f40b3e0efbc4 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Tue, 22 Aug 2023 12:49:30 -0700 Subject: [PATCH 08/13] be compatible to peer and peer_name --- internal/resource/http/http.go | 6 +++++- internal/resource/http/http_test.go | 28 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/internal/resource/http/http.go b/internal/resource/http/http.go index aaf505a6f64..9ec8d62e86f 100644 --- a/internal/resource/http/http.go +++ b/internal/resource/http/http.go @@ -180,9 +180,13 @@ func parseParams(r *http.Request) (tenancy *pbresource.Tenancy, params map[strin if namespace == "" { namespace = query.Get("ns") } + peer := query.Get("peer") + if peer == "" { + peer = query.Get("peer_name") + } tenancy = &pbresource.Tenancy{ Partition: query.Get("partition"), - PeerName: query.Get("peer"), + PeerName: peer, Namespace: namespace, } diff --git a/internal/resource/http/http_test.go b/internal/resource/http/http_test.go index 3d5956116b8..1edef161fb2 100644 --- a/internal/resource/http/http_test.go +++ b/internal/resource/http/http_test.go @@ -56,7 +56,7 @@ func TestResourceHandler_InputValidation(t *testing.T) { testCases := []testCase{ { description: "missing resource name", - request: httptest.NewRequest("PUT", "/?partition=default&peer=local&namespace=default", strings.NewReader(` + request: httptest.NewRequest("PUT", "/?partition=default&peer_name=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -72,7 +72,7 @@ func TestResourceHandler_InputValidation(t *testing.T) { }, { description: "wrong schema", - request: httptest.NewRequest("PUT", "/keith-urban?partition=default&peer=local&namespace=default", strings.NewReader(` + request: httptest.NewRequest("PUT", "/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -88,7 +88,7 @@ func TestResourceHandler_InputValidation(t *testing.T) { }, { description: "no id", - request: httptest.NewRequest("DELETE", "/?partition=default&peer=local&namespace=default", strings.NewReader("")), + request: httptest.NewRequest("DELETE", "/?partition=default&peer_name=local&namespace=default", strings.NewReader("")), response: httptest.NewRecorder(), expectedResponseCode: http.StatusBadRequest, }, @@ -118,7 +118,7 @@ func TestResourceWriteHandler(t *testing.T) { t.Run("should be blocked if the token is not authorized", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default", strings.NewReader(` + req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -139,7 +139,7 @@ func TestResourceWriteHandler(t *testing.T) { t.Run("should write to the resource backend", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default", strings.NewReader(` + req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -179,7 +179,7 @@ func TestResourceWriteHandler(t *testing.T) { t.Run("should update the record with version parameter", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default&version=1", strings.NewReader(` + req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&version=1", strings.NewReader(` { "metadata": { "foo": "bar" @@ -204,7 +204,7 @@ func TestResourceWriteHandler(t *testing.T) { t.Run("should fail the update if the resource's version doesn't match the version of the existing resource", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default&version=1", strings.NewReader(` + req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&version=1", strings.NewReader(` { "metadata": { "foo": "bar" @@ -225,7 +225,7 @@ func TestResourceWriteHandler(t *testing.T) { t.Run("should write to the resource backend with owner", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v1/artist/keith-urban-v1?partition=default&peer=local&namespace=default", strings.NewReader(` + req := httptest.NewRequest("PUT", "/demo/v1/artist/keith-urban-v1?partition=default&peer_name=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -348,7 +348,7 @@ func TestResourceReadHandler(t *testing.T) { t.Run("Read resource", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default&consistent", nil) + req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&consistent", nil) req.Header.Add("x-consul-token", testACLTokenArtistReadPolicy) @@ -363,7 +363,7 @@ func TestResourceReadHandler(t *testing.T) { t.Run("should not be found if resource not exist", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/demo/v2/artist/keith-not-exist?partition=default&peer=local&namespace=default&consistent", nil) + req := httptest.NewRequest("GET", "/demo/v2/artist/keith-not-exist?partition=default&peer_name=local&namespace=default&consistent", nil) req.Header.Add("x-consul-token", testACLTokenArtistReadPolicy) @@ -374,7 +374,7 @@ func TestResourceReadHandler(t *testing.T) { t.Run("should be blocked if the token is not authorized", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default&consistent", nil) + req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&consistent", nil) req.Header.Add("x-consul-token", fakeToken) @@ -402,7 +402,7 @@ func TestResourceDeleteHandler(t *testing.T) { createResource(t, handler, nil) deleteRsp := httptest.NewRecorder() - deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default", strings.NewReader("")) + deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader("")) deletReq.Header.Add("x-consul-token", testACLTokenArtistReadPolicy) @@ -415,7 +415,7 @@ func TestResourceDeleteHandler(t *testing.T) { createResource(t, handler, nil) deleteRsp := httptest.NewRecorder() - deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default", strings.NewReader("")) + deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader("")) deletReq.Header.Add("x-consul-token", testACLTokenArtistWritePolicy) @@ -441,7 +441,7 @@ func TestResourceDeleteHandler(t *testing.T) { createResource(t, handler, nil) rsp := httptest.NewRecorder() - req := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer=local&namespace=default&version=1", strings.NewReader("")) + req := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&version=1", strings.NewReader("")) req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy) req.Header.Add("x-consul-token", testACLTokenArtistListPolicy) From d66dbb5a16f85b9d0847f5b4d0b67640f24bf592 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Tue, 22 Aug 2023 13:09:57 -0700 Subject: [PATCH 09/13] support consistent flag --- command/resource/read/read.go | 13 +++++++------ internal/resource/http/http.go | 3 +++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/command/resource/read/read.go b/command/resource/read/read.go index 56a3213bc4c..af7661085c7 100644 --- a/command/resource/read/read.go +++ b/command/resource/read/read.go @@ -106,10 +106,11 @@ func (c *cmd) Run(args []string) int { return 1 } opts = &api.QueryOptions{ - Namespace: c.http.Namespace(), - Partition: c.http.Partition(), - Peer: c.http.PeerName(), - Token: c.http.Token(), + Namespace: c.http.Namespace(), + Partition: c.http.Partition(), + Peer: c.http.PeerName(), + Token: c.http.Token(), + RequireConsistent: c.http.Stale(), } } @@ -164,9 +165,9 @@ func (c *cmd) Help() string { const synopsis = "Read resource information" const help = ` -Usage: consul resource read [type] [name] -partition= -namespace= -peer= -consistent= -json +Usage: consul resource read [type] [name] -partition= -namespace= -peer= -Reads the resource specified by the given type, name, partition, namespace, peer and reading mode +Reads the resource specified by the given type, name, partition, namespace and peer and outputs its JSON representation. Example: diff --git a/internal/resource/http/http.go b/internal/resource/http/http.go index 9ec8d62e86f..8692a3a0339 100644 --- a/internal/resource/http/http.go +++ b/internal/resource/http/http.go @@ -199,6 +199,9 @@ func parseParams(r *http.Request) (tenancy *pbresource.Tenancy, params map[strin params["resourceName"] = resourceName params["version"] = query.Get("version") params["namePrefix"] = query.Get("name_prefix") + // coming from command line + params["consistent"] = query.Get("RequireConsistent") + // coming from http client if _, ok := query["consistent"]; ok { params["consistent"] = "true" } From 47bdf63c7e773e9d065014b8d8a0839351d0b3ee Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Thu, 31 Aug 2023 11:56:14 -0700 Subject: [PATCH 10/13] code refactor, add tests --- command/resource/read/read.go | 34 ++++++++++++++++++++++++------ command/resource/read/read_test.go | 5 +++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/command/resource/read/read.go b/command/resource/read/read.go index af7661085c7..777bc294f12 100644 --- a/command/resource/read/read.go +++ b/command/resource/read/read.go @@ -5,6 +5,7 @@ package read import ( "encoding/json" + "errors" "flag" "fmt" "strings" @@ -49,9 +50,13 @@ func (c *cmd) Run(args []string) int { var resourceName string var opts *api.QueryOptions + if len(args) == 0 { + c.UI.Error("Please provide subcommands or flags") + return 1 + } if args[0] == "-f" { if err := c.flags.Parse(args); err != nil { - if err == flag.ErrHelp { + if errors.Is(err, flag.ErrHelp) { return 0 } c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) @@ -82,7 +87,7 @@ func (c *cmd) Run(args []string) int { Token: c.http.Token(), } } else { - c.UI.Error("Please input file path") + c.UI.Error("Please provide the HCL file path") return 1 } } else { @@ -95,7 +100,7 @@ func (c *cmd) Run(args []string) int { inputArgs := args[2:] if err := c.flags.Parse(inputArgs); err != nil { - if err == flag.ErrHelp { + if errors.Is(err, flag.ErrHelp) { return 0 } c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) @@ -110,7 +115,7 @@ func (c *cmd) Run(args []string) int { Partition: c.http.Partition(), Peer: c.http.PeerName(), Token: c.http.Token(), - RequireConsistent: c.http.Stale(), + RequireConsistent: !c.http.Stale(), } } @@ -165,12 +170,27 @@ func (c *cmd) Help() string { const synopsis = "Read resource information" const help = ` -Usage: consul resource read [type] [name] -partition= -namespace= -peer= +Usage: You have two options to read the resource specified by the given +type, name, partition, namespace and peer and outputs its JSON representation. -Reads 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.v1alpha1.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.v1alpha1.Service") + Name = "card-processor" + Tenancy { + Namespace = "payments" + Partition = "billing" + PeerName = "eu" + } +} ` diff --git a/command/resource/read/read_test.go b/command/resource/read/read_test.go index d04a715cc3e..f2552fb4013 100644 --- a/command/resource/read/read_test.go +++ b/command/resource/read/read_test.go @@ -19,6 +19,11 @@ func TestResourceReadInvalidArgs(t *testing.T) { } cases := map[string]tc{ + "empty args": { + args: []string{}, + expectedCode: 1, + expectedErrMsg: "Please provide subcommands or flags", + }, "missing file path": { args: []string{"-f"}, expectedCode: 1, From 1208d20d4e8b372f8e1ba4fdd8c5eb53bcc833b7 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Thu, 31 Aug 2023 12:46:37 -0700 Subject: [PATCH 11/13] to lower request string --- api/resource.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/resource.go b/api/resource.go index c80fc3e4443..97baf35934d 100644 --- a/api/resource.go +++ b/api/resource.go @@ -5,6 +5,7 @@ package api import ( "fmt" + "strings" ) type Resource struct { @@ -23,7 +24,7 @@ func (c *Client) Resource() *Resource { } func (resource *Resource) Read(gvk *GVK, resourceName string, q *QueryOptions) (map[string]interface{}, error) { - r := resource.c.newRequest("GET", fmt.Sprintf("/api/%s/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind, resourceName)) + 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 { From d2ff5dc4255393e4b749ffd4e2cccdaad79d2270 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Fri, 1 Sep 2023 14:07:08 -0700 Subject: [PATCH 12/13] refactor and add test --- command/resource/read/read.go | 22 ++++++++++++---------- command/resource/read/read_test.go | 7 ++++++- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/command/resource/read/read.go b/command/resource/read/read.go index 777bc294f12..58fde392c56 100644 --- a/command/resource/read/read.go +++ b/command/resource/read/read.go @@ -51,17 +51,18 @@ func (c *cmd) Run(args []string) int { var opts *api.QueryOptions if len(args) == 0 { - c.UI.Error("Please provide subcommands or flags") + c.UI.Error("Please provide required arguments") return 1 } - if args[0] == "-f" { - if err := c.flags.Parse(args); err != nil { - if errors.Is(err, flag.ErrHelp) { - return 0 - } + + 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 } + } + + if c.flags.Lookup("f").Value.String() != "" { if c.filePath != "" { data, err := helpers.LoadDataSourceNoRaw(c.filePath, nil) if err != nil { @@ -87,10 +88,14 @@ func (c *cmd) Run(args []string) int { Token: c.http.Token(), } } else { - c.UI.Error("Please provide the HCL file path") + c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) return 1 } } else { + if len(args) < 2 { + c.UI.Error("Must specify two arguments: resource type and resource name") + return 1 + } var err error gvk, resourceName, err = getTypeAndResourceName(args) if err != nil { @@ -142,9 +147,6 @@ func (c *cmd) Run(args []string) int { } func getTypeAndResourceName(args []string) (gvk *api.GVK, resourceName string, e error) { - if len(args) < 2 { - return nil, "", fmt.Errorf("Must specify two arguments: resource type and resource name") - } if strings.HasPrefix(args[1], "-") { return nil, "", fmt.Errorf("Must provide resource name right after type") } diff --git a/command/resource/read/read_test.go b/command/resource/read/read_test.go index f2552fb4013..69e9654779b 100644 --- a/command/resource/read/read_test.go +++ b/command/resource/read/read_test.go @@ -19,10 +19,15 @@ func TestResourceReadInvalidArgs(t *testing.T) { } cases := map[string]tc{ + "nil args": { + args: nil, + expectedCode: 1, + expectedErrMsg: "Please provide required arguments", + }, "empty args": { args: []string{}, expectedCode: 1, - expectedErrMsg: "Please provide subcommands or flags", + expectedErrMsg: "Please provide required arguments", }, "missing file path": { args: []string{"-f"}, From cb5fbabbbcf425685f9025e32a3e3c90718d27dc Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Tue, 5 Sep 2023 08:21:42 -0700 Subject: [PATCH 13/13] add missing consistent flag --- command/resource/read/read.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/command/resource/read/read.go b/command/resource/read/read.go index 58fde392c56..022f64099f7 100644 --- a/command/resource/read/read.go +++ b/command/resource/read/read.go @@ -82,10 +82,11 @@ func (c *cmd) Run(args []string) int { } resourceName = parsedResource.Id.GetName() opts = &api.QueryOptions{ - Namespace: parsedResource.Id.Tenancy.GetNamespace(), - Partition: parsedResource.Id.Tenancy.GetPartition(), - Peer: parsedResource.Id.Tenancy.GetPeerName(), - Token: c.http.Token(), + Namespace: parsedResource.Id.Tenancy.GetNamespace(), + Partition: parsedResource.Id.Tenancy.GetPartition(), + Peer: parsedResource.Id.Tenancy.GetPeerName(), + Token: c.http.Token(), + RequireConsistent: !c.http.Stale(), } } else { c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))