Skip to content
44 changes: 44 additions & 0 deletions api/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package api

import (
"fmt"
"strings"
)

type Resource struct {
c *Client
}

type GVK struct {
Group string
Version string
Kind string
}

// Config returns a handle to the Config endpoints
func (c *Client) Resource() *Resource {
return &Resource{c}
}

func (resource *Resource) Read(gvk *GVK, resourceName string, q *QueryOptions) (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 closeResponseBody(resp)
if err := requireOK(resp); err != nil {
return nil, err
}

var out map[string]interface{}
if err := decodeBody(resp, &out); err != nil {
return nil, err
}

return out, nil
}
11 changes: 11 additions & 0 deletions command/flags/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type HTTPFlags struct {
// multi-tenancy flags
namespace StringValue
partition StringValue
peer StringValue
}

func (f *HTTPFlags) ClientFlags() *flag.FlagSet {
Expand Down Expand Up @@ -109,6 +110,10 @@ func (f *HTTPFlags) Partition() string {
return f.partition.String()
}

func (f *HTTPFlags) PeerName() string {
return f.peer.String()
}

func (f *HTTPFlags) Stale() bool {
if f.stale.v == nil {
return false
Expand Down Expand Up @@ -174,3 +179,9 @@ func (f *HTTPFlags) AddPartitionFlag(fs *flag.FlagSet) {
"from the request's ACL token, or will default to the `default` admin partition. "+
"Admin Partitions are a Consul Enterprise feature.")
}

func (f *HTTPFlags) AddPeerName() *flag.FlagSet {
fs := flag.NewFlagSet("", flag.ContinueOnError)
fs.Var(&f.peer, "peer", "Specifies the name of peer to query. By default, it is `local`.")
return fs
}
Comment on lines +183 to +187
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this added specifically for the resource API? I'm wondering if we should hold on for peering as it's not clear how we will implement the api? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

@dhiaayachi
I'm not sure how the peer flag is used by the backend, I added this flag here based on the AC of the ticket, it basically requires the peer information, that's why I added it here.

Please let me know if the information here is redundant or not.

Copy link
Contributor

Choose a reason for hiding this comment

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

@dhiaayachi The peer field is part of the resource gRPC API hence it's included in both the HTTP side and the CLI side.
The pending discussion/work is about whether peer should be moved outside the tenancy block but I am not sure what you mean by "we are not clear how we will implement the api" can you elaborate?

Copy link
Contributor

@dhiaayachi dhiaayachi Sep 1, 2023

Choose a reason for hiding this comment

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

IMO peering tenancy should be different fields, as they represent different values/concepts but I think the backend discussion could happen independently from this

4 changes: 4 additions & 0 deletions command/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ import (
peerlist "github.com/hashicorp/consul/command/peering/list"
peerread "github.com/hashicorp/consul/command/peering/read"
"github.com/hashicorp/consul/command/reload"
"github.com/hashicorp/consul/command/resource"
resourceread "github.com/hashicorp/consul/command/resource/read"
"github.com/hashicorp/consul/command/rtt"
"github.com/hashicorp/consul/command/services"
svcsderegister "github.com/hashicorp/consul/command/services/deregister"
Expand Down Expand Up @@ -238,6 +240,8 @@ func RegisteredCommands(ui cli.Ui) map[string]mcli.CommandFactory {
entry{"peering list", func(ui cli.Ui) (cli.Command, error) { return peerlist.New(ui), nil }},
entry{"peering read", func(ui cli.Ui) (cli.Command, error) { return peerread.New(ui), nil }},
entry{"reload", func(ui cli.Ui) (cli.Command, error) { return reload.New(ui), nil }},
entry{"resource", func(cli.Ui) (cli.Command, error) { return resource.New(), nil }},
entry{"resource read", func(ui cli.Ui) (cli.Command, error) { return resourceread.New(ui), nil }},
entry{"rtt", func(ui cli.Ui) (cli.Command, error) { return rtt.New(ui), nil }},
entry{"services", func(cli.Ui) (cli.Command, error) { return services.New(), nil }},
entry{"services register", func(ui cli.Ui) (cli.Command, error) { return svcsregister.New(ui), nil }},
Expand Down
199 changes: 199 additions & 0 deletions command/resource/read/read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package read

import (
"encoding/json"
"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/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 {
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)
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(),
RequireConsistent: !c.http.Stale(),
}
} 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 = 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 {
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(),
RequireConsistent: !c.http.Stale(),
}
}

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

entry, err := client.Resource().Read(gvk, resourceName, opts)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", gvk, resourceName, err))
return 1
}

b, err := json.MarshalIndent(entry, "", " ")
if err != nil {
c.UI.Error("Failed to encode output data")
return 1
}

c.UI.Info(string(b))
return 0
}

func 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
}

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.v1alpha1.Service card-processor -partition=billing -namespace=payments -peer=eu
$ consul resource read -f resource.hcl

In resource.hcl, it could be:
ID {
Type = gvk("catalog.v1alpha1.Service")
Name = "card-processor"
Tenancy {
Namespace = "payments"
Partition = "billing"
PeerName = "eu"
}
}
`
92 changes: 92 additions & 0 deletions command/resource/read/read_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 read

import (
"testing"

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

func TestResourceReadInvalidArgs(t *testing.T) {
t.Parallel()

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

cases := map[string]tc{
"nil args": {
args: nil,
expectedCode: 1,
expectedErrMsg: "Please provide required arguments",
},
"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"},
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())
})
}
}

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

//_, _, err := client.Resource().Apply()
//require.NoError(t, err)
//
//args := []string{}
//
//code := c.Run(args)
//require.Equal(t, 0, code)
}
Loading