file removal

pull/20342/head
Xinyi Wang 2024-01-24 15:42:36 -08:00
parent 7bb2c7cf13
commit 207ba8d1ef
19 changed files with 384 additions and 2984 deletions

View File

@ -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 }},

View File

@ -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] <resource>
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"
}
`

View File

@ -1,228 +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"
PeerName = "local"
}
}
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",
"peerName": "local"
},
"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())
})
}
}

View File

@ -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,77 +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(),
Peer: parsedResource.Id.Tenancy.GetPeerName(),
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
}
@ -154,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"
@ -171,13 +124,9 @@ Usage: consul resource apply [options] <resource>
$ 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:
@ -185,9 +134,9 @@ Usage: consul resource apply [options] <resource>
Type = gvk("group.version.kind")
Name = "resource-name"
Tenancy {
Namespace = "default"
Partition = "default"
PeerName = "local"
Partition = "default"
Namespace = "default"
PeerName = "local"
}
}

View File

@ -5,6 +5,7 @@ package apply
import (
"errors"
"fmt"
"io"
"testing"
@ -12,7 +13,7 @@ import (
"github.com/stretchr/testify/require"
"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 +23,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 +46,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 +54,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 +68,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 +75,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 +94,8 @@ func TestResourceApplyCommand_StdIn(t *testing.T) {
Type = gvk("demo.v2.Artist")
Name = "korn"
Tenancy {
Namespace = "default"
Partition = "default"
Namespace = "default"
PeerName = "local"
}
}
@ -128,17 +115,19 @@ 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)
require.Empty(t, ui.ErrorWriter.String())
expected := readResource(t, a, []string{"demo.v2.Artist", "korn"})
// 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)
//require.Contains(t, ui.OutputWriter.String(), expected)
})
t.Run("json", func(t *testing.T) {
@ -156,8 +145,8 @@ func TestResourceApplyCommand_StdIn(t *testing.T) {
"id": {
"name": "korn",
"tenancy": {
"namespace": "default",
"partition": "default",
"namespace": "default",
"peerName": "local"
},
"type": {
@ -177,17 +166,19 @@ 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)
require.Empty(t, ui.ErrorWriter.String())
expected := readResource(t, a, []string{"demo.v2.Artist", "korn"})
// 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)
//require.Contains(t, ui.OutputWriter.String(), expected)
})
}
@ -209,7 +200,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"},

File diff suppressed because it is too large Load Diff

View File

@ -1,164 +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(),
PeerName: c.resourceFlags.Peername(),
}
}
// 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=<default> -namespace=<default> -peer=<local>
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"
}
}
`

View File

@ -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")
})
}
}

View File

@ -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())
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(),
Peer: parsedResource.Id.Tenancy.GetPeerName(),
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,31 +96,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(),
Peer: c.http.PeerName(),
Token: c.http.Token(),
resourceTenancy = &pbresource.Tenancy{
Partition: c.resourceFlags.Partition(),
Namespace: c.resourceFlags.Namespace(),
PeerName: c.resourceFlags.Peername(),
}
}
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
}
@ -164,8 +156,8 @@ ID {
Type = gvk("catalog.v2beta1.Service")
Name = "card-processor"
Tenancy {
Namespace = "payments"
Partition = "billing"
Namespace = "payments"
PeerName = "eu"
}
}

View File

@ -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())

View File

@ -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"
)
@ -54,76 +52,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)
@ -167,108 +99,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); {
@ -319,3 +149,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
}

View File

@ -1,193 +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(),
PeerName: c.resourceFlags.Peername(),
}
}
// 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=<default> -namespace=<default> -peer=<local>
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"
}
}
`

View File

@ -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())
})
}
}

View File

@ -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())
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(),
Peer: parsedResource.Id.Tenancy.GetPeerName(),
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,34 +105,36 @@ 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(),
Peer: c.http.PeerName(),
Token: c.http.Token(),
RequireConsistent: !c.http.Stale(),
resourceTenancy = &pbresource.Tenancy{
Partition: c.resourceFlags.Partition(),
Namespace: c.resourceFlags.Namespace(),
PeerName: c.resourceFlags.Peername(),
}
}
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
@ -140,26 +144,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 {
@ -170,7 +165,7 @@ 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=<default> -namespace=<default> -peer=<local>
or
@ -181,19 +176,18 @@ and outputs in JSON format.
Example:
$ consul resource list catalog.v2beta1.Service card-processor -partition=billing -namespace=payments -peer=eu
$ consul resource list catalog.v2beta1.Service -p=card -partition=billing -namespace=payments -peer=eu
$ 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"
PeerName = "local"
Partition = "default"
Namespace = "default"
PeerName = "local"
}
}
`

View File

@ -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,10 +53,21 @@ func TestResourceListCommand(t *testing.T) {
name: "sample output",
output: "\"name\": \"korn\"",
extraArgs: []string{
"demo.v2.artist",
"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",
},
},
{
@ -69,7 +85,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",
}
@ -86,9 +102,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
@ -100,7 +119,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{},
@ -130,11 +149,11 @@ func TestResourceListInvalidArgs(t *testing.T) {
},
"file argument with resource type": {
args: []string{
"demo.v2.artist",
"demo.v2.Artist",
"-partition=default",
"-namespace=default",
"-peer=local",
"-partition=default",
"-http-addr=" + a.HTTPAddr(),
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort),
"-token=root",
"-f=demo.hcl",
},
@ -144,23 +163,23 @@ func TestResourceListInvalidArgs(t *testing.T) {
"resource type invalid": {
args: []string{
"test",
"-partition=default",
"-namespace=default",
"-peer=local",
"-partition=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",
"-peer=local",
"-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"),
},
}

View File

@ -1,172 +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(),
PeerName: c.resourceFlags.Peername(),
}
}
// 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=<default> -namespace=<default> -peer=<local>
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"
}
}
`

View File

@ -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)
})
}
}

View File

@ -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())
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(),
Peer: parsedResource.Id.Tenancy.GetPeerName(),
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,33 +97,36 @@ 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(),
Peer: c.http.PeerName(),
Token: c.http.Token(),
RequireConsistent: !c.http.Stale(),
resourceTenancy = &pbresource.Tenancy{
Partition: c.resourceFlags.Partition(),
Namespace: c.resourceFlags.Namespace(),
PeerName: c.resourceFlags.Peername(),
}
}
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
@ -171,12 +161,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"
PeerName = "eu"
}
Type = gvk("catalog.v2beta1.Service")
Name = "card-processor"
Tenancy {
Partition = "billing"
Namespace = "payments"
PeerName = "eu"
}
}
`

View File

@ -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", "-peer=local"},
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)
})
}