mirror of https://github.com/hashicorp/consul
Browse Source
* Revert "refactor the resource client (#20343)" This reverts commitpull/20712/head3c5cb04b0f
. * Revert "clean up http client (#20342)" This reverts commit2b89025eab
. * remove deprecated peer * fix the typo * remove forwarding test as it tests grpc, should add it back
wangxinyi7
9 months ago
committed by
GitHub
30 changed files with 3282 additions and 861 deletions
@ -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" |
||||
} |
||||
` |
@ -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()) |
||||
}) |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -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 |
||||
} |
@ -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" |
||||
} |
||||
} |
||||
` |
@ -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") |
||||
}) |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
||||
|
||||
}) |
||||
} |
||||
} |
@ -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" |
||||
} |
||||
} |
||||
` |
@ -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()) |
||||
}) |
||||
} |
||||
} |
@ -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" |
||||
} |
||||
} |
||||
` |
@ -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) |
||||
}) |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
Loading…
Reference in new issue