Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions api/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package api

import (
"fmt"
"net/http"
"strings"
)

Expand Down Expand Up @@ -42,3 +43,17 @@ func (resource *Resource) Read(gvk *GVK, resourceName string, q *QueryOptions) (

return out, nil
}

func (resource *Resource) Delete(gvk *GVK, resourceName string, q *QueryOptions) error {
r := resource.c.newRequest("DELETE", 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 closeResponseBody(resp)
if err := requireHttpCodes(resp, http.StatusNoContent); err != nil {
return err
}
return nil
}
2 changes: 2 additions & 0 deletions command/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import (
peerread "github.com/hashicorp/consul/command/peering/read"
"github.com/hashicorp/consul/command/reload"
"github.com/hashicorp/consul/command/resource"
resourcedelete "github.com/hashicorp/consul/command/resource/delete"
resourceread "github.com/hashicorp/consul/command/resource/read"
"github.com/hashicorp/consul/command/rtt"
"github.com/hashicorp/consul/command/services"
Expand Down Expand Up @@ -242,6 +243,7 @@ func RegisteredCommands(ui cli.Ui) map[string]mcli.CommandFactory {
entry{"reload", func(ui cli.Ui) (cli.Command, error) { return reload.New(ui), nil }},
entry{"resource", func(cli.Ui) (cli.Command, error) { return resource.New(), nil }},
entry{"resource read", func(ui cli.Ui) (cli.Command, error) { return resourceread.New(ui), nil }},
entry{"resource delete", func(ui cli.Ui) (cli.Command, error) { return resourcedelete.New(ui), nil }},
entry{"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 }},
Expand Down
173 changes: 173 additions & 0 deletions command/resource/delete/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package delete

import (
"errors"
"flag"
"fmt"

"github.com/mitchellh/cli"

"github.com/hashicorp/consul/agent/consul"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/helpers"
"github.com/hashicorp/consul/command/resource"
"github.com/hashicorp/consul/internal/resourcehcl"
)

func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}

type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string

filePath string
}

func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.http = &flags.HTTPFlags{}
c.flags.StringVar(&c.filePath, "f", "", "File path with resource definition")
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
flags.Merge(c.flags, c.http.MultiTenancyFlags())
flags.Merge(c.flags, c.http.AddPeerName())
c.help = flags.Usage(help, c.flags)
}

func (c *cmd) Run(args []string) int {
var gvk *api.GVK
var resourceName string
var opts *api.QueryOptions

if len(args) == 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this check since we can rely on flags.args for required arguments

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, removed this part.

c.UI.Error("Please provide required arguments")
return 1
}

if err := c.flags.Parse(args); err != nil {
if !errors.Is(err, flag.ErrHelp) {
c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err))
return 1
}
}

if c.flags.Lookup("f").Value.String() != "" {
if c.filePath != "" {
data, err := helpers.LoadDataSourceNoRaw(c.filePath, nil)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use the helper function ParseResourceFromFile here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, updated

if err != nil {
c.UI.Error(fmt.Sprintf("Failed to load data: %v", err))
return 1
}
parsedResource, err := resourcehcl.Unmarshal([]byte(data), consul.NewTypeRegistry())
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err))
return 1
}

gvk = &api.GVK{
Group: parsedResource.Id.Type.GetGroup(),
Version: parsedResource.Id.Type.GetGroupVersion(),
Kind: parsedResource.Id.Type.GetKind(),
}
resourceName = parsedResource.Id.GetName()
opts = &api.QueryOptions{
Namespace: parsedResource.Id.Tenancy.GetNamespace(),
Partition: parsedResource.Id.Tenancy.GetPartition(),
Peer: parsedResource.Id.Tenancy.GetPeerName(),
Token: c.http.Token(),
}
} else {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1
}
} else {
if len(args) < 2 {
c.UI.Error("Must specify two arguments: resource type and resource name")
return 1
}
var err error
gvk, resourceName, err = resource.GetTypeAndResourceName(args)
if err != nil {
c.UI.Error(fmt.Sprintf("Your argument format is incorrect: %s", err))
return 1
}

inputArgs := args[2:]
if err := c.flags.Parse(inputArgs); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, here we can use the helper function ParseInputParams

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, updated

if errors.Is(err, flag.ErrHelp) {
return 0
}
c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err))
return 1
}
if c.filePath != "" {
c.UI.Error("You need to provide all information in the HCL file if provide its file path")
return 1
}
opts = &api.QueryOptions{
Namespace: c.http.Namespace(),
Partition: c.http.Partition(),
Peer: c.http.PeerName(),
Token: c.http.Token(),
}
}

client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}

if err := client.Resource().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", gvk.Group, gvk.Version, gvk.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.v1alpha1.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.v1alpha1.Service")
Name = "card-processor"
Tenancy {
Namespace = "payments"
Partition = "billing"
PeerName = "eu"
}
}
`
92 changes: 92 additions & 0 deletions command/resource/delete/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package delete

import (
"testing"

"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)

func TestResourceDeleteInvalidArgs(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a test case where the resource type is in an invalid format
example: args: []string{"test"}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, as discussed above, we throw error if it's not the format of a.b.c, and added one test case above

t.Parallel()

type tc struct {
args []string
expectedCode int
expectedErrMsg string
}

cases := map[string]tc{
"nil args": {
args: nil,
expectedCode: 1,
expectedErrMsg: "Please provide required arguments",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have consistent error messaging across all the CLI commands. The messaging should start with Your argument format is incorrect: ...

See PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, I picked up the changes in that PR into this PR(only read.go and read_test.go)

},
"empty args": {
args: []string{},
expectedCode: 1,
expectedErrMsg: "Please provide required arguments",
},
"missing file path": {
args: []string{"-f"},
expectedCode: 1,
expectedErrMsg: "Please input file path",
},
"provide type and name": {
args: []string{"a.b.c"},
expectedCode: 1,
expectedErrMsg: "Must specify two arguments: resource type and resource name",
},
"provide type and name with -f": {
args: []string{"a.b.c", "name", "-f", "test.hcl"},
expectedCode: 1,
expectedErrMsg: "You need to provide all information in the HCL file if provide its file path",
},
"provide type and name with -f and other flags": {
args: []string{"a.b.c", "name", "-f", "test.hcl", "-namespace", "default"},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just show a warning and not throw error in this case

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, changed

expectedCode: 1,
expectedErrMsg: "You need to provide all information in the HCL file if provide its file path",
},
"does not provide resource name after type": {
args: []string{"a.b.c", "-namespace", "default"},
expectedCode: 1,
expectedErrMsg: "Must provide resource name right after type",
},
}

for desc, tc := range cases {
t.Run(desc, func(t *testing.T) {
ui := cli.NewMockUi()
c := New(ui)

require.Equal(t, tc.expectedCode, c.Run(tc.args))
require.NotEmpty(t, ui.ErrorWriter.String())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing expectedErrMsg check here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

})
}
}

func TestResourceRead(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The apply command is merged so you should be able to write test cases now 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, added more test cases here(both read and delete)

// TODO: add read test after apply checked in
//if testing.Short() {
// t.Skip("too slow for testing.Short")
//}
//
//t.Parallel()
//
//a := agent.NewTestAgent(t, ``)
//defer a.Shutdown()
//client := a.Client()
//
//ui := cli.NewMockUi()
//c := New(ui)

//_, _, err := client.Resource().Apply()
//require.NoError(t, err)
//
//args := []string{}
//
//code := c.Run(args)
//require.Equal(t, 0, code)
}
25 changes: 25 additions & 0 deletions command/resource/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package resource

import (
"fmt"
"strings"

"github.com/hashicorp/consul/api"
)

func GetTypeAndResourceName(args []string) (gvk *api.GVK, resourceName string, e error) {
// 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")
}

s := strings.Split(args[0], ".")
gvk = &api.GVK{
Group: s[0],
Version: s[1],
Kind: s[2],
}
Comment on lines +52 to +56

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause a panic if the resource type is not in a.b.c format.
We need a check here to prevent that

if len(s) < 3 {
		return nil, "", fmt.Errorf("Must include resource type argument in group.verion.kind format")
	}

Also, lets add a test case for it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated, and added one more test case for it.
Notice I restrict the condition to

if len(s) != 3 {}


resourceName = args[1]
return
}
20 changes: 2 additions & 18 deletions command/resource/read/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import (
"errors"
"flag"
"fmt"
"strings"

"github.com/mitchellh/cli"

"github.com/hashicorp/consul/agent/consul"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/helpers"
"github.com/hashicorp/consul/command/resource"
"github.com/hashicorp/consul/internal/resourcehcl"
)

Expand Down Expand Up @@ -98,7 +98,7 @@ func (c *cmd) Run(args []string) int {
return 1
}
var err error
gvk, resourceName, err = getTypeAndResourceName(args)
gvk, resourceName, err = resource.GetTypeAndResourceName(args)
if err != nil {
c.UI.Error(fmt.Sprintf("Your argument format is incorrect: %s", err))
return 1
Expand Down Expand Up @@ -147,22 +147,6 @@ func (c *cmd) Run(args []string) int {
return 0
}

func getTypeAndResourceName(args []string) (gvk *api.GVK, resourceName string, e error) {
if strings.HasPrefix(args[1], "-") {
return nil, "", fmt.Errorf("Must provide resource name right after type")
}

s := strings.Split(args[0], ".")
gvk = &api.GVK{
Group: s[0],
Version: s[1],
Kind: s[2],
}

resourceName = args[1]
return
}

func (c *cmd) Synopsis() string {
return synopsis
}
Expand Down