revert grpc to http (#20716)

* Revert "refactor the resource client (#20343)"

This reverts commit 3c5cb04b0f.

* Revert "clean up http client (#20342)"

This reverts commit 2b89025eab.

* remove deprecated peer

* fix the typo

* remove forwarding test as it tests grpc, should add it back
This commit is contained in:
wangxinyi7 2024-02-23 12:27:49 -08:00 committed by GitHub
parent e72152465f
commit b1bd6ab91a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 3307 additions and 886 deletions

View File

@ -115,9 +115,13 @@ 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"
@ -256,10 +260,15 @@ 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 apply", func(ui cli.Ui) (cli.Command, error) { return resourceapply.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{"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 list", func(ui cli.Ui) (cli.Command, error) { return resourcelist.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

@ -0,0 +1,150 @@
// 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

@ -0,0 +1,226 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package apply
import (
"errors"
"fmt"
"io"
"testing"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/sdk/freeport"
"github.com/hashicorp/consul/testrpc"
)
func TestResourceApplyCommand(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
availablePort := freeport.GetOne(t)
a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort))
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
t.Cleanup(func() {
a.Shutdown()
})
cases := []struct {
name string
output string
args []string
}{
{
name: "sample output",
args: []string{"-f=../testdata/demo.hcl"},
output: "demo.v2.Artist 'korn' created.",
},
{
name: "nested data format",
args: []string{"-f=../testdata/nested_data.hcl"},
output: "mesh.v2beta1.Destinations 'api' created.",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ui := cli.NewMockUi()
c := New(ui)
args := []string{
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort),
"-token=root",
}
args = append(args, tc.args...)
code := c.Run(args)
require.Equal(t, 0, code)
require.Empty(t, ui.ErrorWriter.String())
require.Contains(t, ui.OutputWriter.String(), tc.output)
})
}
}
func TestResourceApplyCommand_StdIn(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
availablePort := freeport.GetOne(t)
a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort))
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
t.Cleanup(func() {
a.Shutdown()
})
t.Run("hcl", func(t *testing.T) {
stdinR, stdinW := io.Pipe()
ui := cli.NewMockUi()
c := New(ui)
c.testStdin = stdinR
stdInput := `ID {
Type = gvk("demo.v2.Artist")
Name = "korn"
Tenancy {
Partition = "default"
Namespace = "default"
}
}
Data {
Name = "Korn"
Genre = "GENRE_METAL"
}
Metadata = {
"foo" = "bar"
}`
go func() {
stdinW.Write([]byte(stdInput))
stdinW.Close()
}()
args := []string{
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort),
"-token=root",
"-f",
"-",
}
code := c.Run(args)
require.Equal(t, 0, code)
require.Empty(t, ui.ErrorWriter.String())
// Todo: make up the read result check after finishing the read command
//expected := readResource(t, a, []string{"demo.v2.Artist", "korn"})
require.Contains(t, ui.OutputWriter.String(), "demo.v2.Artist 'korn' created.")
//require.Contains(t, ui.OutputWriter.String(), expected)
})
t.Run("json", func(t *testing.T) {
stdinR, stdinW := io.Pipe()
ui := cli.NewMockUi()
c := New(ui)
c.testStdin = stdinR
stdInput := `{
"data": {
"genre": "GENRE_METAL",
"name": "Korn"
},
"id": {
"name": "korn",
"tenancy": {
"partition": "default",
"namespace": "default"
},
"type": {
"group": "demo",
"groupVersion": "v2",
"kind": "Artist"
}
},
"metadata": {
"foo": "bar"
}
}`
go func() {
stdinW.Write([]byte(stdInput))
stdinW.Close()
}()
args := []string{
"-f",
"-",
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort),
"-token=root",
}
code := c.Run(args)
require.Equal(t, 0, code)
require.Empty(t, ui.ErrorWriter.String())
// Todo: make up the read result check after finishing the read command
//expected := readResource(t, a, []string{"demo.v2.Artist", "korn"})
require.Contains(t, ui.OutputWriter.String(), "demo.v2.Artist 'korn' created.")
//require.Contains(t, ui.OutputWriter.String(), expected)
})
}
func TestResourceApplyInvalidArgs(t *testing.T) {
t.Parallel()
type tc struct {
args []string
expectedCode int
expectedErr error
}
cases := map[string]tc{
"no file path": {
args: []string{"-f"},
expectedCode: 1,
expectedErr: errors.New("Failed to parse args: flag needs an argument: -f"),
},
"missing required flag": {
args: []string{},
expectedCode: 1,
expectedErr: errors.New("Required '-f' flag was not provided to specify where to load the resource content from"),
},
"file parsing failure": {
args: []string{"-f=../testdata/invalid.hcl"},
expectedCode: 1,
expectedErr: errors.New("Failed to decode resource from input file"),
},
"file not found": {
args: []string{"-f=../testdata/test.hcl"},
expectedCode: 1,
expectedErr: errors.New("Failed to load data: Failed to read file: open ../testdata/test.hcl: no such file or directory"),
},
}
for desc, tc := range cases {
t.Run(desc, func(t *testing.T) {
ui := cli.NewMockUi()
c := New(ui)
code := c.Run(tc.args)
require.Equal(t, tc.expectedCode, code)
require.Contains(t, ui.ErrorWriter.String(), tc.expectedErr.Error())
})
}
}

View File

@ -11,8 +11,13 @@ 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 {
@ -24,7 +29,7 @@ func New(ui cli.Ui) *cmd {
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
grpcFlags *client.GRPCFlags
http *flags.HTTPFlags
help string
filePath string
@ -37,9 +42,31 @@ func (c *cmd) init() {
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)
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
}
func (c *cmd) Run(args []string) int {
@ -48,55 +75,76 @@ func (c *cmd) Run(args []string) int {
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
if input == "" && len(c.flags.Args()) > 0 {
input = c.flags.Arg(0)
}
parsedResource, err := client.ParseResourceInput(input, c.testStdin)
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")
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)
config := api.DefaultConfig()
c.http.MergeOntoConfig(config)
resourceClient, err := client.NewClient(config)
if err != nil {
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}
// write resource
entry, err := resourceClient.Apply(parsedResource)
res := resource.Resource{C: resourceClient}
opts := &client.QueryOptions{
Namespace: parsedResource.Id.Tenancy.GetNamespace(),
Partition: parsedResource.Id.Tenancy.GetPartition(),
Token: c.http.Token(),
}
gvk := &resource.GVK{
Group: parsedResource.Id.Type.GetGroup(),
Version: parsedResource.Id.Type.GetGroupVersion(),
Kind: parsedResource.Id.Type.GetKind(),
}
writeRequest, err := makeWriteRequest(parsedResource)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing resource %s/%s: %v", parsedResource.Id.Type, parsedResource.Id.GetName(), err))
c.UI.Error(fmt.Sprintf("Error parsing hcl input: %v", err))
return 1
}
// display response
b, err := json.MarshalIndent(entry, "", client.JSON_INDENT)
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, "", " ")
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))
c.UI.Info(fmt.Sprintf("%s.%s.%s '%s' created.", gvk.Group, gvk.Version, gvk.Kind, parsedResource.Id.GetName()))
c.UI.Info(string(b))
return 0
}
@ -105,7 +153,7 @@ func (c *cmd) Synopsis() string {
}
func (c *cmd) Help() string {
return client.Usage(c.help, nil)
return flags.Usage(c.help, nil)
}
const synopsis = "Writes/updates resource information"
@ -122,9 +170,13 @@ 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 -f - < demo.hcl
$ consul resource apply -
Sample demo.hcl:

View File

@ -5,7 +5,6 @@ package apply
import (
"errors"
"fmt"
"io"
"testing"
@ -14,7 +13,6 @@ import (
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/command/resource/read"
"github.com/hashicorp/consul/sdk/freeport"
"github.com/hashicorp/consul/testrpc"
)
@ -24,14 +22,10 @@ func TestResourceApplyCommand(t *testing.T) {
}
t.Parallel()
availablePort := freeport.GetOne(t)
a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort))
a := agent.NewTestAgent(t, ``)
defer a.Shutdown()
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
t.Cleanup(func() {
a.Shutdown()
})
cases := []struct {
name string
output string
@ -47,6 +41,11 @@ func TestResourceApplyCommand(t *testing.T) {
args: []string{"-f=../testdata/nested_data.hcl"},
output: "mesh.v2beta1.Destinations 'api' created.",
},
{
name: "file path with no flag",
args: []string{"../testdata/nested_data.hcl"},
output: "mesh.v2beta1.Destinations 'api' created.",
},
}
for _, tc := range cases {
@ -55,7 +54,7 @@ func TestResourceApplyCommand(t *testing.T) {
c := New(ui)
args := []string{
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort),
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
@ -69,6 +68,23 @@ 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")
@ -76,14 +92,10 @@ func TestResourceApplyCommand_StdIn(t *testing.T) {
t.Parallel()
availablePort := freeport.GetOne(t)
a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort))
a := agent.NewTestAgent(t, ``)
defer a.Shutdown()
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
t.Cleanup(func() {
a.Shutdown()
})
t.Run("hcl", func(t *testing.T) {
stdinR, stdinW := io.Pipe()
@ -95,8 +107,8 @@ func TestResourceApplyCommand_StdIn(t *testing.T) {
Type = gvk("demo.v2.Artist")
Name = "korn"
Tenancy {
Partition = "default"
Namespace = "default"
Partition = "default"
}
}
@ -115,18 +127,17 @@ func TestResourceApplyCommand_StdIn(t *testing.T) {
}()
args := []string{
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort),
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-f",
"-",
}
code := c.Run(args)
require.Equal(t, 0, code, ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
expected := readResource(t, a, []string{"demo.v2.Artist", "korn"})
require.Contains(t, ui.OutputWriter.String(), "demo.v2.Artist 'korn' created.")
readUI := readResource(t, []string{"demo.v2.Artist", "korn"}, availablePort)
require.Contains(t, ui.OutputWriter.String(), readUI.OutputWriter.String())
require.Contains(t, ui.OutputWriter.String(), expected)
})
t.Run("json", func(t *testing.T) {
@ -144,8 +155,8 @@ func TestResourceApplyCommand_StdIn(t *testing.T) {
"id": {
"name": "korn",
"tenancy": {
"partition": "default",
"namespace": "default"
"namespace": "default",
"partition": "default"
},
"type": {
"group": "demo",
@ -164,18 +175,17 @@ func TestResourceApplyCommand_StdIn(t *testing.T) {
}()
args := []string{
"-f",
"-",
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort),
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-",
}
code := c.Run(args)
require.Equal(t, 0, code, ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
expected := readResource(t, a, []string{"demo.v2.Artist", "korn"})
require.Contains(t, ui.OutputWriter.String(), "demo.v2.Artist 'korn' created.")
readUI := readResource(t, []string{"demo.v2.Artist", "korn"}, availablePort)
require.Contains(t, ui.OutputWriter.String(), readUI.OutputWriter.String())
require.Contains(t, ui.OutputWriter.String(), expected)
})
}
@ -197,7 +207,7 @@ func TestResourceApplyInvalidArgs(t *testing.T) {
"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"),
expectedErr: errors.New("Incorrect argument format: Must provide exactly one positional argument to specify the resource to write"),
},
"file parsing failure": {
args: []string{"-f=../testdata/invalid.hcl"},
@ -223,21 +233,3 @@ func TestResourceApplyInvalidArgs(t *testing.T) {
})
}
}
func readResource(t *testing.T, args []string, port int) *cli.MockUi {
readUi := cli.NewMockUi()
readCmd := read.New(readUi)
flags := []string{
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", port),
"-token=root",
}
args = append(args, flags...)
code := readCmd.Run(args)
require.Equal(t, 0, code)
require.Empty(t, readUi.ErrorWriter.String())
return readUi
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,65 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package client
import (
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"github.com/hashicorp/consul/proto-public/pbresource"
)
type GRPCClient struct {
Client pbresource.ResourceServiceClient
Config *GRPCConfig
Conn *grpc.ClientConn
}
func NewGRPCClient(config *GRPCConfig) (*GRPCClient, error) {
conn, err := dial(config)
if err != nil {
return nil, fmt.Errorf("error dialing grpc: %+v", err)
}
return &GRPCClient{
Client: pbresource.NewResourceServiceClient(conn),
Config: config,
Conn: conn,
}, nil
}
func dial(c *GRPCConfig) (*grpc.ClientConn, error) {
err := checkCertificates(c)
if err != nil {
return nil, err
}
var dialOpts []grpc.DialOption
if c.GRPCTLS {
tlsConfig, err := SetupTLSConfig(c)
if err != nil {
return nil, fmt.Errorf("failed to setup tls config when tried to establish grpc call: %w", err)
}
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
} else {
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
return grpc.Dial(c.Address, dialOpts...)
}
func checkCertificates(c *GRPCConfig) error {
if c.GRPCTLS {
certFileEmpty := c.CertFile == ""
keyFileEmpty := c.KeyFile == ""
// both files need to be empty or both files need to be provided
if certFileEmpty != keyFileEmpty {
return fmt.Errorf("you have to provide client certificate file and key file at the same time " +
"if you intend to communicate in TLS/SSL mode")
}
}
return nil
}

View File

@ -5,28 +5,13 @@ package client
import (
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/hashicorp/go-rootcerts"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/anypb"
"github.com/hashicorp/consul/agent/consul"
"github.com/hashicorp/consul/command/helpers"
"github.com/hashicorp/consul/internal/resourcehcl"
"github.com/hashicorp/consul/proto-public/pbresource"
)
const JSON_INDENT = " "
// tls.Config is used to establish communication in TLS mode
func SetupTLSConfig(c *GRPCConfig) (*tls.Config, error) {
tlsConfig := &tls.Config{
@ -106,194 +91,3 @@ func (t *TValue[T]) Merge(onto *T) error {
}
return nil
}
type OuterResource struct {
ID *ID `json:"id"`
Owner *ID `json:"owner"`
Generation string `json:"generation"`
Version string `json:"version"`
Metadata map[string]any `json:"metadata"`
Data map[string]any `json:"data"`
}
type Tenancy struct {
Partition string `json:"partition"`
Namespace string `json:"namespace"`
}
type Type struct {
Group string `json:"group"`
GroupVersion string `json:"groupVersion"`
Kind string `json:"kind"`
}
type ID struct {
Name string `json:"name"`
Tenancy Tenancy `json:"tenancy"`
Type Type `json:"type"`
UID string `json:"uid"`
}
func ParseResourceFromFile(filePath string) (*pbresource.Resource, error) {
return ParseResourceInput(filePath, nil)
}
func ParseResourceInput(filePath string, stdin io.Reader) (*pbresource.Resource, error) {
data, err := helpers.LoadDataSourceNoRaw(filePath, stdin)
if err != nil {
return nil, fmt.Errorf("Failed to load data: %v", err)
}
var parsedResource *pbresource.Resource
if isHCL([]byte(data)) {
parsedResource, err = resourcehcl.Unmarshal([]byte(data), consul.NewTypeRegistry())
} else {
parsedResource, err = parseJson(data)
}
if err != nil {
return nil, fmt.Errorf("Failed to decode resource from input: %v", err)
}
return parsedResource, nil
}
func ParseInputParams(inputArgs []string, flags *flag.FlagSet) error {
if err := flags.Parse(inputArgs); err != nil {
if !errors.Is(err, flag.ErrHelp) {
return fmt.Errorf("Failed to parse args: %v", err)
}
}
return nil
}
func GetTypeAndResourceName(args []string) (resourceType *pbresource.Type, resourceName string, e error) {
if len(args) < 2 {
return nil, "", fmt.Errorf("Must specify two arguments: resource type and resource name")
}
// it has to be resource name after the type
if strings.HasPrefix(args[1], "-") {
return nil, "", fmt.Errorf("Must provide resource name right after type")
}
resourceName = args[1]
resourceType, e = InferTypeFromResourceType(args[0])
return resourceType, resourceName, e
}
func InferTypeFromResourceType(resourceType string) (*pbresource.Type, error) {
s := strings.Split(resourceType, ".")
switch length := len(s); {
// only kind is provided
case length == 1:
kindToGVKMap := BuildKindToGVKMap()
kind := strings.ToLower(s[0])
switch len(kindToGVKMap[kind]) {
// no g.v.k is found
case 0:
return nil, fmt.Errorf("The shorthand name does not map to any existing resource type, please check `consul api-resources`")
// only one is found
case 1:
// infer gvk from resource kind
gvkSplit := strings.Split(kindToGVKMap[kind][0], ".")
return &pbresource.Type{
Group: gvkSplit[0],
GroupVersion: gvkSplit[1],
Kind: gvkSplit[2],
}, nil
// it alerts error if any conflict is found
default:
return nil, fmt.Errorf("The shorthand name has conflicts %v, please use the full name", kindToGVKMap[s[0]])
}
case length == 3:
return &pbresource.Type{
Group: s[0],
GroupVersion: s[1],
Kind: s[2],
}, nil
default:
return nil, fmt.Errorf("Must provide resource type argument with either in group.version.kind format or its shorthand name")
}
}
func BuildKindToGVKMap() map[string][]string {
// this use the local copy of registration to build map
typeRegistry := consul.NewTypeRegistry()
kindToGVKMap := map[string][]string{}
for _, r := range typeRegistry.Types() {
gvkString := fmt.Sprintf("%s.%s.%s", r.Type.Group, r.Type.GroupVersion, r.Type.Kind)
kindKey := strings.ToLower(r.Type.Kind)
if len(kindToGVKMap[kindKey]) == 0 {
kindToGVKMap[kindKey] = []string{gvkString}
} else {
kindToGVKMap[kindKey] = append(kindToGVKMap[kindKey], gvkString)
}
}
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

@ -7,7 +7,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTValue(t *testing.T) {
@ -69,27 +68,3 @@ func TestTValue(t *testing.T) {
assert.Equal(t, onto, true)
})
}
func Test_parseJson(t *testing.T) {
tests := []struct {
name string
js string
wantErr bool
}{
{"valid resource", "{\n \"data\": {\n \"genre\": \"GENRE_METAL\",\n \"name\": \"Korn\"\n },\n \"generation\": \"01HAYWBPV1KMT2KWECJ6CEWDQ0\",\n \"id\": {\n \"name\": \"korn\",\n \"tenancy\": {\n \"namespace\": \"default\",\n \"partition\": \"default\" },\n \"type\": {\n \"group\": \"demo\",\n \"groupVersion\": \"v2\",\n \"kind\": \"Artist\"\n },\n \"uid\": \"01HAYWBPV1KMT2KWECJ4NW88S1\"\n },\n \"metadata\": {\n \"foo\": \"bar\"\n },\n \"version\": \"18\"\n}", false},
{"invalid resource", "{\n \"data\": {\n \"genre\": \"GENRE_METAL\",\n \"name\": \"Korn\"\n },\n \"id\": {\n \"name\": \"korn\",\n \"tenancy\": {\n \"namespace\": \"default\",\n \"partition\": \"default\" },\n \"type\": \"\"\n },\n \"metadata\": {\n \"foo\": \"bar\"\n }\n}\n", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseJson(tt.js)
if tt.wantErr {
require.Error(t, err)
require.Nil(t, got)
} else {
require.NoError(t, err)
require.NotNil(t, got)
}
})
}
}

View File

@ -0,0 +1,163 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package delete
import (
"errors"
"flag"
"fmt"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/resource"
"github.com/hashicorp/consul/command/resource/client"
"github.com/hashicorp/consul/proto-public/pbresource"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
grpcFlags *client.GRPCFlags
resourceFlags *client.ResourceFlags
help string
filePath string
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(&c.filePath, "f", "",
"File path with resource definition")
c.grpcFlags = &client.GRPCFlags{}
c.resourceFlags = &client.ResourceFlags{}
client.MergeFlags(c.flags, c.grpcFlags.ClientFlags())
client.MergeFlags(c.flags, c.resourceFlags.ResourceFlags())
c.help = client.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
var resourceType *pbresource.Type
var resourceTenancy *pbresource.Tenancy
var resourceName string
if err := c.flags.Parse(args); err != nil {
if !errors.Is(err, flag.ErrHelp) {
c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err))
return 1
}
c.UI.Error(fmt.Sprintf("Failed to run delete command: %v", err))
return 1
}
// collect resource type, name and tenancy
if c.flags.Lookup("f").Value.String() != "" {
if c.filePath == "" {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1
}
parsedResource, err := resource.ParseResourceFromFile(c.filePath)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err))
return 1
}
if parsedResource == nil {
c.UI.Error("Unable to parse the file argument")
return 1
}
resourceType = parsedResource.Id.Type
resourceTenancy = parsedResource.Id.Tenancy
resourceName = parsedResource.Id.Name
} else {
var err error
resourceType, resourceName, err = resource.GetTypeAndResourceName(args)
if err != nil {
c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err))
return 1
}
inputArgs := args[2:]
err = resource.ParseInputParams(inputArgs, c.flags)
if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err))
return 1
}
if c.filePath != "" {
c.UI.Error("Incorrect argument format: File argument is not needed when resource information is provided with the command")
return 1
}
resourceTenancy = &pbresource.Tenancy{
Partition: c.resourceFlags.Partition(),
Namespace: c.resourceFlags.Namespace(),
}
}
// initialize client
config, err := client.LoadGRPCConfig(nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error loading config: %s", err))
return 1
}
c.grpcFlags.MergeFlagsIntoGRPCConfig(config)
resourceClient, err := client.NewGRPCClient(config)
if err != nil {
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}
// delete resource
res := resource.ResourceGRPC{C: resourceClient}
err = res.Delete(resourceType, resourceTenancy, resourceName)
if err != nil {
c.UI.Error(fmt.Sprintf("Error deleting resource %s/%s: %v", resourceType, resourceName, err))
return 1
}
c.UI.Info(fmt.Sprintf("%s.%s.%s/%s deleted", resourceType.Group, resourceType.GroupVersion, resourceType.Kind, resourceName))
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Delete resource information"
const help = `
Usage: You have two options to delete the resource specified by the given
type, name, partition, namespace and peer and outputs its JSON representation.
consul resource delete [type] [name] -partition=<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

@ -0,0 +1,164 @@
// 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"},
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"},
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,9 @@ 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"
)
@ -24,8 +26,7 @@ func New(ui cli.Ui) *cmd {
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
grpcFlags *client.GRPCFlags
resourceFlags *client.ResourceFlags
http *flags.HTTPFlags
help string
filePath string
@ -33,37 +34,31 @@ type cmd struct {
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)
c.http = &flags.HTTPFlags{}
c.flags.StringVar(&c.filePath, "f", "", "File path with resource definition")
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
flags.Merge(c.flags, c.http.MultiTenancyFlags())
// TODO(peering/v2) add back ability to query peers
// flags.Merge(c.flags, c.http.AddPeerName())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
var resourceType *pbresource.Type
var resourceTenancy *pbresource.Tenancy
var gvk *resource.GVK
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 == "" {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1
}
parsedResource, err := client.ParseResourceFromFile(c.filePath)
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
@ -74,19 +69,37 @@ func (c *cmd) Run(args []string) int {
return 1
}
resourceType = parsedResource.Id.Type
resourceTenancy = parsedResource.Id.Tenancy
resourceName = parsedResource.Id.Name
gvk = &resource.GVK{
Group: parsedResource.Id.Type.GetGroup(),
Version: parsedResource.Id.Type.GetGroupVersion(),
Kind: parsedResource.Id.Type.GetKind(),
}
resourceName = parsedResource.Id.GetName()
opts = &client.QueryOptions{
Namespace: parsedResource.Id.Tenancy.GetNamespace(),
Partition: parsedResource.Id.Tenancy.GetPartition(),
Token: c.http.Token(),
}
} else {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1
}
} else {
var err error
resourceType, resourceName, err = client.GetTypeAndResourceName(args)
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
}
inputArgs := args[2:]
err = client.ParseInputParams(inputArgs, c.flags)
err = resource.ParseInputParams(inputArgs, c.flags)
if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err))
return 1
@ -95,33 +108,30 @@ 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
}
resourceTenancy = &pbresource.Tenancy{
Partition: c.resourceFlags.Partition(),
Namespace: c.resourceFlags.Namespace(),
opts = &client.QueryOptions{
Namespace: c.http.Namespace(),
Partition: c.http.Partition(),
Token: c.http.Token(),
}
}
// 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)
config := api.DefaultConfig()
c.http.MergeOntoConfig(config)
resourceClient, err := client.NewClient(config)
if err != nil {
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}
// delete resource
err = resourceClient.Delete(resourceType, resourceTenancy, resourceName)
if err != nil {
c.UI.Error(fmt.Sprintf("Error deleting resource %s/%s: %v", resourceType, resourceName, err))
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))
return 1
}
c.UI.Info(fmt.Sprintf("%s.%s.%s/%s deleted", resourceType.Group, resourceType.GroupVersion, resourceType.Kind, resourceName))
c.UI.Info(fmt.Sprintf("%s.%s.%s/%s deleted", gvk.Group, gvk.Version, gvk.Kind, resourceName))
return 0
}
@ -136,7 +146,7 @@ func (c *cmd) Help() string {
const synopsis = "Delete resource information"
const help = `
Usage: You have two options to delete the resource specified by the given
type, name, partition, namespace and outputs its JSON representation.
type, name, partition and namespace and outputs its JSON representation.
consul resource delete [type] [name] -partition=<default> -namespace=<default>
consul resource delete -f [resource_file_path]
@ -153,8 +163,8 @@ ID {
Type = gvk("catalog.v2beta1.Service")
Name = "card-processor"
Tenancy {
Partition = "billing"
Namespace = "payments"
Partition = "billing"
}
}
`

View File

@ -4,7 +4,6 @@ package delete
import (
"errors"
"fmt"
"testing"
"github.com/mitchellh/cli"
@ -12,7 +11,6 @@ import (
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/command/resource/apply"
"github.com/hashicorp/consul/sdk/freeport"
"github.com/hashicorp/consul/testrpc"
)
@ -86,12 +84,12 @@ func TestResourceDeleteInvalidArgs(t *testing.T) {
}
}
func createResource(t *testing.T, port int) {
func createResource(t *testing.T, a *agent.TestAgent) {
applyUi := cli.NewMockUi()
applyCmd := apply.New(applyUi)
args := []string{
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", port),
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
@ -109,18 +107,14 @@ func TestResourceDelete(t *testing.T) {
t.Parallel()
availablePort := freeport.GetOne(t)
a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort))
a := agent.NewTestAgent(t, ``)
defer a.Shutdown()
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
t.Cleanup(func() {
a.Shutdown()
})
defaultCmdArgs := []string{
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort),
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
cases := []struct {
name string
args []string
@ -153,7 +147,7 @@ func TestResourceDelete(t *testing.T) {
c := New(ui)
cliArgs := append(tc.args, defaultCmdArgs...)
if tc.createResource {
createResource(t, availablePort)
createResource(t, a)
}
code := c.Run(cliArgs)
require.Empty(t, ui.ErrorWriter.String())

322
command/resource/helper.go Normal file
View File

@ -0,0 +1,322 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package resource
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"strings"
"unicode"
"unicode/utf8"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/anypb"
"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"
)
const JSON_INDENT = " "
type OuterResource struct {
ID *ID `json:"id"`
Owner *ID `json:"owner"`
Generation string `json:"generation"`
Version string `json:"version"`
Metadata map[string]any `json:"metadata"`
Data map[string]any `json:"data"`
}
type Tenancy struct {
Partition string `json:"partition"`
Namespace string `json:"namespace"`
}
// TODO(peering/v2) handle v2 peering in the resource cli
type Type struct {
Group string `json:"group"`
GroupVersion string `json:"groupVersion"`
Kind string `json:"kind"`
}
type ID struct {
Name string `json:"name"`
Tenancy Tenancy `json:"tenancy"`
Type Type `json:"type"`
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)
if err != nil {
return nil, fmt.Errorf("Failed to load data: %v", err)
}
var parsedResource *pbresource.Resource
if isHCL([]byte(data)) {
parsedResource, err = resourcehcl.Unmarshal([]byte(data), consul.NewTypeRegistry())
} else {
parsedResource, err = parseJson(data)
}
if err != nil {
return nil, fmt.Errorf("Failed to decode resource from input: %v", err)
}
return parsedResource, nil
}
func ParseInputParams(inputArgs []string, flags *flag.FlagSet) error {
if err := flags.Parse(inputArgs); err != nil {
if !errors.Is(err, flag.ErrHelp) {
return fmt.Errorf("Failed to parse args: %v", err)
}
}
return nil
}
func GetTypeAndResourceName(args []string) (resourceType *pbresource.Type, resourceName string, e error) {
if len(args) < 2 {
return nil, "", fmt.Errorf("Must specify two arguments: resource type and resource name")
}
// it has to be resource name after the type
if strings.HasPrefix(args[1], "-") {
return nil, "", fmt.Errorf("Must provide resource name right after type")
}
resourceName = args[1]
resourceType, e = InferTypeFromResourceType(args[0])
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); {
// only kind is provided
case length == 1:
kindToGVKMap := BuildKindToGVKMap()
kind := strings.ToLower(s[0])
switch len(kindToGVKMap[kind]) {
// no g.v.k is found
case 0:
return nil, fmt.Errorf("The shorthand name does not map to any existing resource type, please check `consul api-resources`")
// only one is found
case 1:
// infer gvk from resource kind
gvkSplit := strings.Split(kindToGVKMap[kind][0], ".")
return &pbresource.Type{
Group: gvkSplit[0],
GroupVersion: gvkSplit[1],
Kind: gvkSplit[2],
}, nil
// it alerts error if any conflict is found
default:
return nil, fmt.Errorf("The shorthand name has conflicts %v, please use the full name", kindToGVKMap[s[0]])
}
case length == 3:
return &pbresource.Type{
Group: s[0],
GroupVersion: s[1],
Kind: s[2],
}, nil
default:
return nil, fmt.Errorf("Must provide resource type argument with either in group.version.kind format or its shorthand name")
}
}
func BuildKindToGVKMap() map[string][]string {
// this use the local copy of registration to build map
typeRegistry := consul.NewTypeRegistry()
kindToGVKMap := map[string][]string{}
for _, r := range typeRegistry.Types() {
gvkString := fmt.Sprintf("%s.%s.%s", r.Type.Group, r.Type.GroupVersion, r.Type.Kind)
kindKey := strings.ToLower(r.Type.Kind)
if len(kindToGVKMap[kindKey]) == 0 {
kindToGVKMap[kindKey] = []string{gvkString}
} else {
kindToGVKMap[kindKey] = append(kindToGVKMap[kindKey], gvkString)
}
}
return kindToGVKMap
}

View File

@ -0,0 +1,34 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package resource
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_parseJson(t *testing.T) {
tests := []struct {
name string
js string
wantErr bool
}{
{"valid resource", "{\n \"data\": {\n \"genre\": \"GENRE_METAL\",\n \"name\": \"Korn\"\n },\n \"generation\": \"01HAYWBPV1KMT2KWECJ6CEWDQ0\",\n \"id\": {\n \"name\": \"korn\",\n \"tenancy\": {\n \"namespace\": \"default\",\n \"partition\": \"default\"\n },\n \"type\": {\n \"group\": \"demo\",\n \"groupVersion\": \"v2\",\n \"kind\": \"Artist\"\n },\n \"uid\": \"01HAYWBPV1KMT2KWECJ4NW88S1\"\n },\n \"metadata\": {\n \"foo\": \"bar\"\n },\n \"version\": \"18\"\n}", false},
{"invalid resource", "{\n \"data\": {\n \"genre\": \"GENRE_METAL\",\n \"name\": \"Korn\"\n },\n \"id\": {\n \"name\": \"korn\",\n \"tenancy\": {\n \"namespace\": \"default\",\n \"partition\": \"default\"\n },\n \"type\": \"\"\n },\n \"metadata\": {\n \"foo\": \"bar\"\n }\n}\n", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseJson(tt.js)
if tt.wantErr {
require.Error(t, err)
require.Nil(t, got)
} else {
require.NoError(t, err)
require.NotNil(t, got)
}
})
}
}

View File

@ -0,0 +1,192 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package list
import (
"encoding/json"
"errors"
"flag"
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/resource"
"github.com/hashicorp/consul/command/resource/client"
"github.com/hashicorp/consul/proto-public/pbresource"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
grpcFlags *client.GRPCFlags
resourceFlags *client.ResourceFlags
help string
filePath string
prefix string
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(&c.filePath, "f", "",
"File path with resource definition")
c.flags.StringVar(&c.prefix, "p", "",
"Name prefix for listing resources if you need ambiguous match")
c.grpcFlags = &client.GRPCFlags{}
c.resourceFlags = &client.ResourceFlags{}
client.MergeFlags(c.flags, c.grpcFlags.ClientFlags())
client.MergeFlags(c.flags, c.resourceFlags.ResourceFlags())
c.help = client.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
var resourceType *pbresource.Type
var resourceTenancy *pbresource.Tenancy
if err := c.flags.Parse(args); err != nil {
if !errors.Is(err, flag.ErrHelp) {
c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err))
return 1
}
c.UI.Error(fmt.Sprintf("Failed to run list command: %v", err))
return 1
}
// collect resource type, name and tenancy
if c.flags.Lookup("f").Value.String() != "" {
if c.filePath == "" {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1
}
parsedResource, err := resource.ParseResourceFromFile(c.filePath)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err))
return 1
}
if parsedResource == nil {
c.UI.Error("Unable to parse the file argument")
return 1
}
resourceType = parsedResource.Id.Type
resourceTenancy = parsedResource.Id.Tenancy
} else {
var err error
args := c.flags.Args()
if err = validateArgs(args); err != nil {
c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err))
return 1
}
resourceType, err = resource.InferTypeFromResourceType(args[0])
if err != nil {
c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err))
return 1
}
// skip resource type to parse remaining args
inputArgs := c.flags.Args()[1:]
err = resource.ParseInputParams(inputArgs, c.flags)
if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err))
return 1
}
if c.filePath != "" {
c.UI.Error("Incorrect argument format: File argument is not needed when resource information is provided with the command")
return 1
}
resourceTenancy = &pbresource.Tenancy{
Partition: c.resourceFlags.Partition(),
Namespace: c.resourceFlags.Namespace(),
}
}
// initialize client
config, err := client.LoadGRPCConfig(nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error loading config: %s", err))
return 1
}
c.grpcFlags.MergeFlagsIntoGRPCConfig(config)
resourceClient, err := client.NewGRPCClient(config)
if err != nil {
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}
// list resource
res := resource.ResourceGRPC{C: resourceClient}
entry, err := res.List(resourceType, resourceTenancy, c.prefix, c.resourceFlags.Stale())
if err != nil {
c.UI.Error(fmt.Sprintf("Error listing resource %s/%s: %v", resourceType, c.prefix, err))
return 1
}
// display response
b, err := json.MarshalIndent(entry, "", resource.JSON_INDENT)
if err != nil {
c.UI.Error("Failed to encode output data")
return 1
}
c.UI.Info(string(b))
return 0
}
func validateArgs(args []string) error {
if args == nil {
return fmt.Errorf("Must include resource type or flag arguments")
}
if len(args) < 1 {
return fmt.Errorf("Must include resource type argument")
}
if len(args) > 1 && !strings.HasPrefix(args[1], "-") {
return fmt.Errorf("Must include flag arguments after resource type")
}
return nil
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Lists all resources by name prefix"
const help = `
Usage: consul resource list [type] -partition=<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

@ -0,0 +1,192 @@
// 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",
},
},
{
name: "sample output with name prefix",
output: "\"name\": \"korn\"",
extraArgs: []string{
"demo.v2.Artist",
"-p=korn",
"-partition=default",
"-namespace=default",
},
},
{
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",
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",
},
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",
"-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,9 +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 {
@ -26,48 +27,38 @@ func New(ui cli.Ui) *cmd {
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
grpcFlags *client.GRPCFlags
resourceFlags *client.ResourceFlags
http *flags.HTTPFlags
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)
c.http = &flags.HTTPFlags{}
c.flags.StringVar(&c.filePath, "f", "", "File path with resource definition")
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
flags.Merge(c.flags, c.http.MultiTenancyFlags())
// TODO(peering/v2) add back ability to query peers
// flags.Merge(c.flags, c.http.AddPeerName())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
var resourceType *pbresource.Type
var resourceTenancy *pbresource.Tenancy
var gvk *resource.GVK
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 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 := client.ParseResourceFromFile(c.filePath)
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
@ -78,24 +69,32 @@ func (c *cmd) Run(args []string) int {
return 1
}
resourceType = parsedResource.Id.Type
resourceTenancy = parsedResource.Id.Tenancy
gvk = &resource.GVK{
Group: parsedResource.Id.Type.GetGroup(),
Version: parsedResource.Id.Type.GetGroupVersion(),
Kind: parsedResource.Id.Type.GetKind(),
}
opts = &client.QueryOptions{
Namespace: parsedResource.Id.Tenancy.GetNamespace(),
Partition: parsedResource.Id.Tenancy.GetPartition(),
Token: c.http.Token(),
RequireConsistent: !c.http.Stale(),
}
} else {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1
}
} 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 = client.InferTypeFromResourceType(args[0])
// extract resource type
gvk, err = getResourceType(c.flags.Args())
if err != nil {
c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err))
c.UI.Error(fmt.Sprintf("Incorrect argument format: %v", err))
return 1
}
// skip resource type to parse remaining args
inputArgs := c.flags.Args()[1:]
err = client.ParseInputParams(inputArgs, c.flags)
err = resource.ParseInputParams(inputArgs, c.flags)
if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err))
return 1
@ -104,34 +103,33 @@ 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
}
resourceTenancy = &pbresource.Tenancy{
Partition: c.resourceFlags.Partition(),
Namespace: c.resourceFlags.Namespace(),
opts = &client.QueryOptions{
Namespace: c.http.Namespace(),
Partition: c.http.Partition(),
Token: c.http.Token(),
RequireConsistent: !c.http.Stale(),
}
}
// 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)
config := api.DefaultConfig()
c.http.MergeOntoConfig(config)
resourceClient, err := client.NewClient(config)
if err != nil {
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}
// list resource
entry, err := resourceClient.List(resourceType, resourceTenancy, c.prefix, c.resourceFlags.Stale())
res := resource.Resource{C: resourceClient}
entry, err := res.List(gvk, opts)
if err != nil {
c.UI.Error(fmt.Sprintf("Error listing resource %s/%s: %v", resourceType, c.prefix, err))
c.UI.Error(fmt.Sprintf("Error reading resources for type %s: %v", gvk, err))
return 1
}
// display response
b, err := json.MarshalIndent(entry, "", client.JSON_INDENT)
b, err := json.MarshalIndent(entry, "", " ")
if err != nil {
c.UI.Error("Failed to encode output data")
return 1
@ -141,17 +139,26 @@ func (c *cmd) Run(args []string) int {
return 0
}
func validateArgs(args []string) error {
if args == nil {
return fmt.Errorf("Must include resource type or flag arguments")
}
func getResourceType(args []string) (gvk *resource.GVK, e error) {
if len(args) < 1 {
return fmt.Errorf("Must include resource type argument")
return nil, fmt.Errorf("Must include resource type argument")
}
// it should not have resource name
if len(args) > 1 && !strings.HasPrefix(args[1], "-") {
return fmt.Errorf("Must include flag arguments after resource type")
return nil, fmt.Errorf("Must include flag arguments after resource type")
}
return nil
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
}
func (c *cmd) Synopsis() string {
@ -162,28 +169,29 @@ func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Lists all resources by name prefix"
const synopsis = "Reads all resources by type"
const help = `
Usage: consul resource list [type] -partition=<default> -namespace=<default>
or
consul resource list -f [path/to/file.hcl]
Lists all the resources specified by the type under the given partition, namespace
Lists all the resources specified by the type under the given partition and namespace
and outputs in JSON format.
Example:
$ consul resource list catalog.v2beta1.Service -p=card -partition=billing -namespace=payments
$ consul resource list catalog.v2beta1.Service card-processor -partition=billing -namespace=payments
$ consul resource list -f=demo.hcl -p=card
$ consul resource list -f=demo.hcl
Sample demo.hcl:
ID {
Type = gvk("group.version.kind")
Name = "resource-name"
Tenancy {
Partition = "default"
Namespace = "default"
Partition = "default"
}
}
`

View File

@ -5,13 +5,11 @@ 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"
@ -25,19 +23,16 @@ func TestResourceListCommand(t *testing.T) {
}
t.Parallel()
availablePort := freeport.GetOne(t)
a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort))
a := agent.NewTestAgent(t, ``)
defer a.Shutdown()
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),
"-http-addr=" + a.HTTPAddr(),
"-token=root",
})
require.Equal(t, 0, code)
@ -53,19 +48,9 @@ func TestResourceListCommand(t *testing.T) {
name: "sample output",
output: "\"name\": \"korn\"",
extraArgs: []string{
"demo.v2.Artist",
"-partition=default",
"demo.v2.artist",
"-namespace=default",
},
},
{
name: "sample output with name prefix",
output: "\"name\": \"korn\"",
extraArgs: []string{
"demo.v2.Artist",
"-p=korn",
"-partition=default",
"-namespace=default",
},
},
{
@ -83,7 +68,7 @@ func TestResourceListCommand(t *testing.T) {
c := New(ui)
args := []string{
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort),
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
@ -100,12 +85,9 @@ func TestResourceListCommand(t *testing.T) {
func TestResourceListInvalidArgs(t *testing.T) {
t.Parallel()
availablePort := freeport.GetOne(t)
a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort))
a := agent.NewTestAgent(t, ``)
defer a.Shutdown()
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
t.Cleanup(func() {
a.Shutdown()
})
type tc struct {
args []string
@ -117,7 +99,7 @@ func TestResourceListInvalidArgs(t *testing.T) {
"nil args": {
args: nil,
expectedCode: 1,
expectedErr: errors.New("Incorrect argument format: Must include resource type or flag arguments"),
expectedErr: errors.New("Incorrect argument format: Must include resource type argument"),
},
"minimum args required": {
args: []string{},
@ -147,10 +129,10 @@ func TestResourceListInvalidArgs(t *testing.T) {
},
"file argument with resource type": {
args: []string{
"demo.v2.Artist",
"-partition=default",
"demo.v2.artist",
"-namespace=default",
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort),
"-partition=default",
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-f=demo.hcl",
},
@ -160,21 +142,21 @@ func TestResourceListInvalidArgs(t *testing.T) {
"resource type invalid": {
args: []string{
"test",
"-partition=default",
"-namespace=default",
"-partition=default",
},
expectedCode: 1,
expectedErr: errors.New("Incorrect argument format: The shorthand name does not map to any existing resource type"),
expectedErr: errors.New("Must include resource type argument in group.version.kind format"),
},
"resource name is provided": {
args: []string{
"demo.v2.Artist",
"demo.v2.artist",
"test",
"-namespace=default",
"-partition=default",
},
expectedCode: 1,
expectedErr: errors.New("Incorrect argument format: Must include flag arguments after resource type"),
expectedErr: errors.New("Must include flag arguments after resource type"),
},
}

View File

@ -0,0 +1,171 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package read
import (
"encoding/json"
"errors"
"flag"
"fmt"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/resource"
"github.com/hashicorp/consul/command/resource/client"
"github.com/hashicorp/consul/proto-public/pbresource"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
grpcFlags *client.GRPCFlags
resourceFlags *client.ResourceFlags
help string
filePath string
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(&c.filePath, "f", "",
"File path with resource definition")
c.grpcFlags = &client.GRPCFlags{}
c.resourceFlags = &client.ResourceFlags{}
client.MergeFlags(c.flags, c.grpcFlags.ClientFlags())
client.MergeFlags(c.flags, c.resourceFlags.ResourceFlags())
c.help = client.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
var resourceType *pbresource.Type
var resourceTenancy *pbresource.Tenancy
var resourceName string
if err := c.flags.Parse(args); err != nil {
if !errors.Is(err, flag.ErrHelp) {
c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err))
return 1
}
c.UI.Error(fmt.Sprintf("Failed to run read command: %v", err))
return 1
}
// collect resource type, name and tenancy
if c.flags.Lookup("f").Value.String() != "" {
if c.filePath == "" {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1
}
parsedResource, err := resource.ParseResourceFromFile(c.filePath)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err))
return 1
}
if parsedResource == nil {
c.UI.Error("The parsed resource is nil")
return 1
}
resourceType = parsedResource.Id.Type
resourceTenancy = parsedResource.Id.Tenancy
resourceName = parsedResource.Id.Name
} else {
var err error
resourceType, resourceName, err = resource.GetTypeAndResourceName(args)
if err != nil {
c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err))
return 1
}
inputArgs := args[2:]
err = resource.ParseInputParams(inputArgs, c.flags)
if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err))
return 1
}
if c.filePath != "" {
c.UI.Error("Incorrect argument format: File argument is not needed when resource information is provided with the command")
return 1
}
resourceTenancy = &pbresource.Tenancy{
Partition: c.resourceFlags.Partition(),
Namespace: c.resourceFlags.Namespace(),
}
}
// initialize client
config, err := client.LoadGRPCConfig(nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error loading config: %s", err))
return 1
}
c.grpcFlags.MergeFlagsIntoGRPCConfig(config)
resourceClient, err := client.NewGRPCClient(config)
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
// read resource
res := resource.ResourceGRPC{C: resourceClient}
entry, err := res.Read(resourceType, resourceTenancy, resourceName, c.resourceFlags.Stale())
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", resourceType, resourceName, err))
return 1
}
// display response
b, err := json.MarshalIndent(entry, "", resource.JSON_INDENT)
if err != nil {
c.UI.Error("Failed to encode output data")
return 1
}
c.UI.Info(string(b))
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Read resource information"
const help = `
Usage: You have two options to read the resource specified by the given
type, name, partition, namespace and peer and outputs its JSON representation.
consul resource read [type] [name] -partition=<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

@ -0,0 +1,161 @@
// 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"},
expectedCode: 0,
errMsg: "",
},
{
name: "read resource that doesn't exist",
args: []string{"demo.v2.Artist", "fake-korn", "-partition=default", "-namespace=default"},
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,9 @@ 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"
)
@ -25,8 +27,7 @@ func New(ui cli.Ui) *cmd {
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
grpcFlags *client.GRPCFlags
resourceFlags *client.ResourceFlags
http *flags.HTTPFlags
help string
filePath string
@ -34,60 +35,73 @@ type cmd struct {
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)
c.http = &flags.HTTPFlags{}
c.flags.StringVar(&c.filePath, "f", "", "File path with resource definition")
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
flags.Merge(c.flags, c.http.MultiTenancyFlags())
// TODO(peering/v2) add back ability to query peers
// flags.Merge(c.flags, c.http.AddPeerName())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
var resourceType *pbresource.Type
var resourceTenancy *pbresource.Tenancy
var gvk *resource.GVK
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 == "" {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1
}
parsedResource, err := client.ParseResourceFromFile(c.filePath)
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("The parsed resource is nil")
c.UI.Error("Unable to parse the file argument")
return 1
}
resourceType = parsedResource.Id.Type
resourceTenancy = parsedResource.Id.Tenancy
resourceName = parsedResource.Id.Name
gvk = &resource.GVK{
Group: parsedResource.Id.Type.GetGroup(),
Version: parsedResource.Id.Type.GetGroupVersion(),
Kind: parsedResource.Id.Type.GetKind(),
}
resourceName = parsedResource.Id.GetName()
opts = &client.QueryOptions{
Namespace: parsedResource.Id.Tenancy.GetNamespace(),
Partition: parsedResource.Id.Tenancy.GetPartition(),
Token: c.http.Token(),
RequireConsistent: !c.http.Stale(),
}
} else {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1
}
} else {
var err error
resourceType, resourceName, err = client.GetTypeAndResourceName(args)
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
}
inputArgs := args[2:]
err = client.ParseInputParams(inputArgs, c.flags)
err = resource.ParseInputParams(inputArgs, c.flags)
if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err))
return 1
@ -96,34 +110,32 @@ 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
}
resourceTenancy = &pbresource.Tenancy{
Partition: c.resourceFlags.Partition(),
Namespace: c.resourceFlags.Namespace(),
opts = &client.QueryOptions{
Namespace: c.http.Namespace(),
Partition: c.http.Partition(),
Token: c.http.Token(),
RequireConsistent: !c.http.Stale(),
}
}
// initialize client
config, err := client.LoadGRPCConfig(nil)
config := api.DefaultConfig()
c.http.MergeOntoConfig(config)
resourceClient, err := client.NewClient(config)
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))
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}
// read resource
entry, err := resourceClient.Read(resourceType, resourceTenancy, resourceName, c.resourceFlags.Stale())
res := resource.Resource{C: resourceClient}
entry, err := res.Read(gvk, resourceName, opts)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", resourceType, resourceName, err))
c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", gvk, resourceName, err))
return 1
}
// display response
b, err := json.MarshalIndent(entry, "", client.JSON_INDENT)
b, err := json.MarshalIndent(entry, "", " ")
if err != nil {
c.UI.Error("Failed to encode output data")
return 1
@ -144,7 +156,7 @@ func (c *cmd) Help() string {
const synopsis = "Read resource information"
const help = `
Usage: You have two options to read the resource specified by the given
type, name, partition, namespace and outputs its JSON representation.
type, name, partition and namespace and outputs its JSON representation.
consul resource read [type] [name] -partition=<default> -namespace=<default>
consul resource read -f [resource_file_path]
@ -161,8 +173,8 @@ ID {
Type = gvk("catalog.v2beta1.Service")
Name = "card-processor"
Tenancy {
Partition = "billing"
Namespace = "payments"
Partition = "billing"
}
}
`

View File

@ -4,7 +4,6 @@ package read
import (
"errors"
"fmt"
"testing"
"github.com/mitchellh/cli"
@ -12,7 +11,6 @@ import (
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/command/resource/apply"
"github.com/hashicorp/consul/sdk/freeport"
"github.com/hashicorp/consul/testrpc"
)
@ -86,12 +84,12 @@ func TestResourceReadInvalidArgs(t *testing.T) {
}
}
func createResource(t *testing.T, port int) {
func createResource(t *testing.T, a *agent.TestAgent) {
applyUi := cli.NewMockUi()
applyCmd := apply.New(applyUi)
args := []string{
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", port),
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
@ -109,19 +107,16 @@ func TestResourceRead(t *testing.T) {
t.Parallel()
availablePort := freeport.GetOne(t)
a := agent.NewTestAgent(t, fmt.Sprintf("ports { grpc = %d }", availablePort))
a := agent.NewTestAgent(t, ``)
defer a.Shutdown()
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
t.Cleanup(func() {
a.Shutdown()
})
defaultCmdArgs := []string{
fmt.Sprintf("-grpc-addr=127.0.0.1:%d", availablePort),
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
createResource(t, availablePort)
createResource(t, a)
cases := []struct {
name string
args []string
@ -144,7 +139,7 @@ func TestResourceRead(t *testing.T) {
name: "read resource that doesn't exist",
args: []string{"demo.v2.Artist", "fake-korn", "-partition=default", "-namespace=default"},
expectedCode: 1,
errMsg: "error reading resource: rpc error: code = NotFound desc = resource not found\n",
errMsg: "Error reading resource demo.v2.Artist/fake-korn: Unexpected response code: 404 (rpc error: code = NotFound desc = resource not found)\n",
},
}
@ -154,7 +149,7 @@ func TestResourceRead(t *testing.T) {
c := New(ui)
cliArgs := append(tc.args, defaultCmdArgs...)
code := c.Run(cliArgs)
require.Contains(t, ui.ErrorWriter.String(), tc.errMsg)
require.Equal(t, tc.errMsg, ui.ErrorWriter.String())
require.Equal(t, tc.expectedCode, code)
})
}

View File

@ -0,0 +1,123 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package resource
import (
"context"
"fmt"
"google.golang.org/grpc/metadata"
"github.com/hashicorp/consul/command/resource/client"
"github.com/hashicorp/consul/proto-public/pbresource"
)
const (
HeaderConsulToken = "x-consul-token"
)
type ResourceGRPC struct {
C *client.GRPCClient
}
func (resource *ResourceGRPC) Apply(parsedResource *pbresource.Resource) (*pbresource.Resource, error) {
token, err := resource.C.Config.GetToken()
if err != nil {
return nil, err
}
ctx := context.Background()
if token != "" {
ctx = metadata.AppendToOutgoingContext(context.Background(), HeaderConsulToken, token)
}
defer resource.C.Conn.Close()
writeRsp, err := resource.C.Client.Write(ctx, &pbresource.WriteRequest{Resource: parsedResource})
if err != nil {
return nil, fmt.Errorf("error writing resource: %+v", err)
}
return writeRsp.Resource, err
}
func (resource *ResourceGRPC) Read(resourceType *pbresource.Type, resourceTenancy *pbresource.Tenancy, resourceName string, stale bool) (*pbresource.Resource, error) {
token, err := resource.C.Config.GetToken()
if err != nil {
return nil, err
}
ctx := context.Background()
if !stale {
ctx = metadata.AppendToOutgoingContext(ctx, "x-consul-consistency-mode", "consistent")
}
if token != "" {
ctx = metadata.AppendToOutgoingContext(context.Background(), HeaderConsulToken, token)
}
defer resource.C.Conn.Close()
readRsp, err := resource.C.Client.Read(ctx, &pbresource.ReadRequest{
Id: &pbresource.ID{
Type: resourceType,
Tenancy: resourceTenancy,
Name: resourceName,
},
})
if err != nil {
return nil, fmt.Errorf("error reading resource: %+v", err)
}
return readRsp.Resource, err
}
func (resource *ResourceGRPC) List(resourceType *pbresource.Type, resourceTenancy *pbresource.Tenancy, prefix string, stale bool) ([]*pbresource.Resource, error) {
token, err := resource.C.Config.GetToken()
if err != nil {
return nil, err
}
ctx := context.Background()
if !stale {
ctx = metadata.AppendToOutgoingContext(ctx, "x-consul-consistency-mode", "consistent")
}
if token != "" {
ctx = metadata.AppendToOutgoingContext(context.Background(), HeaderConsulToken, token)
}
defer resource.C.Conn.Close()
listRsp, err := resource.C.Client.List(ctx, &pbresource.ListRequest{
Type: resourceType,
Tenancy: resourceTenancy,
NamePrefix: prefix,
})
if err != nil {
return nil, fmt.Errorf("error listing resource: %+v", err)
}
return listRsp.Resources, err
}
func (resource *ResourceGRPC) Delete(resourceType *pbresource.Type, resourceTenancy *pbresource.Tenancy, resourceName string) error {
token, err := resource.C.Config.GetToken()
if err != nil {
return err
}
ctx := context.Background()
if token != "" {
ctx = metadata.AppendToOutgoingContext(context.Background(), HeaderConsulToken, token)
}
defer resource.C.Conn.Close()
_, err = resource.C.Client.Delete(ctx, &pbresource.DeleteRequest{
Id: &pbresource.ID{
Type: resourceType,
Tenancy: resourceTenancy,
Name: resourceName,
},
})
if err != nil {
return fmt.Errorf("error deleting resource: %+v", err)
}
return nil
}

View File

@ -47,10 +47,6 @@ List resources by type:
$ consul resource list [type] -partition=<default> -namespace=<default>
Delete a resource:
$ consul resource delete [type] [name] -partition=<default> -namespace=<default> -consistent=<false> -json
Run
consul resource <subcommand> -h

View File

@ -1,188 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package resource
import (
"context"
"fmt"
"io"
"testing"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
libcluster "github.com/hashicorp/consul/test/integration/consul-container/libs/cluster"
libtopology "github.com/hashicorp/consul/test/integration/consul-container/libs/topology"
)
const (
RESOURCE_FILE_PATH_ON_HOST = "../../../../../command/resource/testdata/demo.hcl"
CLIENT_CERT_ON_HOST = "../../../../client_certs/client.crt"
CLIENT_KEY_ON_HOST = "../../../../client_certs/client.key"
ROOT_CA_ON_HOST = "../../../../client_certs/rootca.crt"
RESOURCE_FILE_PATH_ON_CONTAINER = "/consul/data/demo.hcl"
CLIENT_CERT_ON_CONTAINER = "/consul/data/client.crt"
CLIENT_KEY_ON_CONTAINER = "/consul/data/client.key"
ROOT_CA_ON_CONTAINER = "/consul/data/rootca.crt"
)
func TestClientForwardToServer(t *testing.T) {
type operation struct {
action func(*testing.T, libcluster.Agent, string, bool) (int, string)
includeToken bool
expectedCode int
expectedMsg string
}
type testCase struct {
description string
operation operation
aclEnabled bool
tlsEnabled bool
verifyIncoming bool
}
testCases := []testCase{
{
description: "The apply request should be forwarded to consul server agent",
operation: operation{
action: applyResource,
includeToken: false,
expectedCode: 0,
expectedMsg: "demo.v2.Artist 'korn' created.",
},
aclEnabled: false,
tlsEnabled: false,
verifyIncoming: false,
},
{
description: "The apply request should be denied if missing token when ACL is enabled",
operation: operation{
action: applyResource,
includeToken: false,
expectedCode: 1,
expectedMsg: "failed getting authorizer: ACL not found",
},
aclEnabled: true,
},
{
description: "The apply request should be allowed if providing token when ACL is enabled",
operation: operation{
action: applyResource,
includeToken: true,
expectedCode: 0,
expectedMsg: "demo.v2.Artist 'korn' created.",
},
aclEnabled: true,
tlsEnabled: false,
verifyIncoming: false,
},
{
description: "The apply request should be forwarded to consul server agent when server is in TLS mode",
operation: operation{
action: applyResource,
includeToken: false,
expectedCode: 0,
expectedMsg: "demo.v2.Artist 'korn' created.",
},
aclEnabled: false,
tlsEnabled: true,
verifyIncoming: false,
},
{
description: "The apply request should be forwarded to consul server agent when server and client are in TLS mode",
operation: operation{
action: applyResource,
includeToken: false,
expectedCode: 0,
expectedMsg: "demo.v2.Artist 'korn' created.",
},
aclEnabled: false,
tlsEnabled: true,
verifyIncoming: true,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.description, func(t *testing.T) {
t.Parallel()
var clientAgent libcluster.Agent
cluster, clientAgent := setupClusterAndClient(t, tc.aclEnabled, tc.tlsEnabled, tc.verifyIncoming)
defer terminate(t, cluster)
// perform actions and validate returned messages
token := ""
if tc.operation.includeToken {
token = cluster.TokenBootstrap
}
code, res := tc.operation.action(t, clientAgent, token, tc.verifyIncoming)
require.Equal(t, tc.operation.expectedCode, code)
require.Contains(t, res, tc.operation.expectedMsg)
})
}
}
func applyResource(t *testing.T, clientAgent libcluster.Agent, token string, verifyIncoming bool) (int, string) {
c := clientAgent.GetConsulContainer()
copyFilesToContainer(t, c, verifyIncoming)
args := []string{"/bin/consul", "resource", "apply", fmt.Sprintf("-f=%s", RESOURCE_FILE_PATH_ON_CONTAINER)}
if token != "" {
args = append(args, fmt.Sprintf("-token=%s", token))
}
if verifyIncoming {
args = append(
args,
"-grpc-tls=true",
"-grpc-addr=127.0.0.1:8503",
fmt.Sprintf("-client-cert=%s", CLIENT_CERT_ON_CONTAINER),
fmt.Sprintf("-client-key=%s", CLIENT_KEY_ON_CONTAINER),
fmt.Sprintf("-ca-file=%s", ROOT_CA_ON_CONTAINER),
)
}
code, reader, err := c.Exec(context.Background(), args)
require.NoError(t, err)
buf, err := io.ReadAll(reader)
require.NoError(t, err)
return code, string(buf)
}
func copyFilesToContainer(t *testing.T, c testcontainers.Container, verifyIncoming bool) {
err := c.CopyFileToContainer(context.Background(), RESOURCE_FILE_PATH_ON_HOST, RESOURCE_FILE_PATH_ON_CONTAINER, 700)
require.NoError(t, err)
if verifyIncoming {
err = c.CopyFileToContainer(context.Background(), CLIENT_CERT_ON_HOST, CLIENT_CERT_ON_CONTAINER, 700)
require.NoError(t, err)
err = c.CopyFileToContainer(context.Background(), CLIENT_KEY_ON_HOST, CLIENT_KEY_ON_CONTAINER, 700)
require.NoError(t, err)
err = c.CopyFileToContainer(context.Background(), ROOT_CA_ON_HOST, ROOT_CA_ON_CONTAINER, 700)
require.NoError(t, err)
}
}
func setupClusterAndClient(t *testing.T, aclEnabled bool, tlsEnabled bool, verifyIncoming bool) (*libcluster.Cluster, libcluster.Agent) {
clusterConfig := &libtopology.ClusterConfig{
NumServers: 1,
NumClients: 1,
LogConsumer: &libtopology.TestLogConsumer{},
BuildOpts: &libcluster.BuildOptions{
Datacenter: "dc1",
InjectAutoEncryption: tlsEnabled,
UseGRPCWithTLS: tlsEnabled,
ACLEnabled: aclEnabled,
},
ApplyDefaultProxySettings: false,
}
if verifyIncoming {
clusterConfig.Cmd = "-hcl=tls { defaults { verify_incoming = true } }"
}
cluster, _, _ := libtopology.NewCluster(t, clusterConfig)
return cluster, cluster.Clients()[0]
}
func terminate(t *testing.T, cluster *libcluster.Cluster) {
err := cluster.Terminate()
require.NoError(t, err)
}