Skip to content

Commit

Permalink
feat: implement apply command
Browse files Browse the repository at this point in the history
  • Loading branch information
JadhavPoonam committed Aug 28, 2023
1 parent d66dbb5 commit a2d81ac
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 0 deletions.
35 changes: 35 additions & 0 deletions api/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package api

import (
"fmt"
"strings"

"github.com/hashicorp/consul/proto-public/pbresource"
)

type Resource struct {
Expand All @@ -17,6 +20,12 @@ type GVK struct {
Kind string
}

type WriteRequest struct {
Metadata map[string]string `json:"metadata"`
Data map[string]string `json:"data"`
Owner *pbresource.ID `json:"owner"`
}

// Config returns a handle to the Config endpoints
func (c *Client) Resource() *Resource {
return &Resource{c}
Expand All @@ -41,3 +50,29 @@ func (resource *Resource) Read(gvk *GVK, resourceName string, q *QueryOptions) (

return out, nil
}

func (resource *Resource) Apply(gvk *GVK, resourceName string, q *QueryOptions, payload *WriteRequest) (map[string]interface{}, *WriteMeta, 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
rtt, resp, err := resource.c.doRequest(r)
if err != nil {
return nil, nil, err
}
defer closeResponseBody(resp)
if err := requireOK(resp); err != nil {
return nil, nil, err
}

wm := &WriteMeta{}
wm.RequestTime = rtt

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

return out, wm, 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"
resourceapply "github.com/hashicorp/consul/command/resource/apply"
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 apply", func(ui cli.Ui) (cli.Command, error) { return resourceapply.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
162 changes: 162 additions & 0 deletions command/resource/apply/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package apply

import (
"encoding/json"
"flag"
"fmt"

"github.com/mitchellh/cli"
"google.golang.org/protobuf/encoding/protojson"

"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"
"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
http *flags.HTTPFlags
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.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
c.help = flags.Usage(help, c.flags)
}

func makeWriteRequest(parsedResource *pbresource.Resource) (payload *api.WriteRequest, error error) {
data, err := protojson.Marshal(parsedResource.Data)
if err != nil {
return nil, fmt.Errorf("unrecognized hcl format: %s", err)
}

var d map[string]string
err = json.Unmarshal(data, &d)
if err != nil {
return nil, fmt.Errorf("unrecognized hcl format: %s", err)
}
delete(d, "@type")

return &api.WriteRequest{
Data: d,
Metadata: parsedResource.GetMetadata(),
Owner: parsedResource.GetOwner(),
}, nil
}

func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
if err == flag.ErrHelp {
return 0
}
c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err))
return 1
}

var parsedResource *pbresource.Resource

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 = parseResource(data)
if err != nil {
c.UI.Error(fmt.Sprintf("Your argument format is incorrect: %s", err))
return 1
}
}

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

opts := &api.QueryOptions{
Namespace: parsedResource.Id.Tenancy.GetNamespace(),
Partition: parsedResource.Id.Tenancy.GetPartition(),
Peer: parsedResource.Id.Tenancy.GetPeerName(),
Token: c.http.Token(),
}

gvk := &api.GVK{
Group: parsedResource.Id.Type.GetGroup(),
Version: parsedResource.Id.Type.GetGroupVersion(),
Kind: parsedResource.Id.Type.GetKind(),
}

writeRequest, err := makeWriteRequest(parsedResource)
if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing hcl input: %v", err))
return 1
}

entry, _, err := client.Resource().Apply(gvk, parsedResource.Id.GetName(), opts, writeRequest)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing resource %s/%s: %v", gvk, parsedResource.Id.GetName(), err))
return 1
}

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

c.UI.Info(fmt.Sprintf("%s.%s.%s '%s' created.", gvk.Group, gvk.Version, gvk.Kind, parsedResource.Id.GetName()))
c.UI.Info(string(b))
return 0
}

func parseResource(data string) (resource *pbresource.Resource, e error) {
// parse the data
raw := []byte(data)
resource, err := resourcehcl.Unmarshal(raw, consul.NewTypeRegistry())
if err != nil {
return nil, fmt.Errorf("Failed to decode resource from input file: %v", err)
}

return resource, nil
}

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

func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}

const synopsis = "Writes/updates resource information"
const help = `
Usage: consul resource apply -f=<file-path>
Writes and/or updates a resource from the definition in the hcl file provided as argument
Example:
$ consul resource apply -f=demo.hcl
`
82 changes: 82 additions & 0 deletions command/resource/apply/apply_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package apply

import (
"errors"
"testing"

"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)

func TestResourceApplyCommand(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}

t.Parallel()
a := agent.NewTestAgent(t, ``)
defer a.Shutdown()
testrpc.WaitForTestAgent(t, a.RPC, "dc1")

cases := []struct {
name string
output string
}{
{
name: "sample output",
output: "demo.v2.Artist 'korn' created.",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ui := cli.NewMockUi()
c := New(ui)
args := []string{
"-f=../testdata/demo.hcl",
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}

code := c.Run(args)
require.Equal(t, 0, code)
require.Empty(t, ui.ErrorWriter.String())
require.Contains(t, ui.OutputWriter.String(), tc.output)
})
}
}

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

type tc struct {
args []string
expectedCode int
expectedErr error
}

cases := map[string]tc{
"missing file path": {
args: []string{"-f"},
expectedCode: 1,
expectedErr: errors.New("Failed to parse args: flag needs an argument: -f"),
},
}

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())
})
}
}
4 changes: 4 additions & 0 deletions command/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ Read a resource:
$ consul resource read [type] [name] -partition=<default> -namespace=<default> -peer=<local> -consistent=<false> -json
Write/update a resource:
$ consul resource apply -f=<file-path>
Run
consul resource <subcommand> -h
Expand Down
18 changes: 18 additions & 0 deletions command/resource/testdata/demo.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
ID {
Type = gvk("demo.v2.Artist")
Name = "korn"
Tenancy {
Namespace = "default"
Partition = "default"
PeerName = "local"
}
}

Data {
Name = "Korn"
Genre = "GENRE_METAL"
}

Metadata = {
"foo" = "bar"
}

0 comments on commit a2d81ac

Please sign in to comment.