refactor the resource client (#20343)

* renaming files
This commit is contained in:
wangxinyi7 2024-01-30 12:33:44 -08:00 committed by GitHub
parent 2b89025eab
commit 3c5cb04b0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 424 additions and 464 deletions

View File

@ -12,7 +12,6 @@ import (
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/resource"
"github.com/hashicorp/consul/command/resource/client" "github.com/hashicorp/consul/command/resource/client"
) )
@ -59,7 +58,7 @@ func (c *cmd) Run(args []string) int {
c.UI.Error("Required '-f' flag was not provided to specify where to load the resource content from") c.UI.Error("Required '-f' flag was not provided to specify where to load the resource content from")
return 1 return 1
} }
parsedResource, err := resource.ParseResourceInput(input, c.testStdin) parsedResource, err := client.ParseResourceInput(input, c.testStdin)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err))
return 1 return 1
@ -83,15 +82,14 @@ func (c *cmd) Run(args []string) int {
} }
// write resource // write resource
res := resource.ResourceGRPC{C: resourceClient} entry, err := resourceClient.Apply(parsedResource)
entry, err := res.Apply(parsedResource)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error writing resource %s/%s: %v", parsedResource.Id.Type, parsedResource.Id.GetName(), err)) c.UI.Error(fmt.Sprintf("Error writing resource %s/%s: %v", parsedResource.Id.Type, parsedResource.Id.GetName(), err))
return 1 return 1
} }
// display response // display response
b, err := json.MarshalIndent(entry, "", resource.JSON_INDENT) b, err := json.MarshalIndent(entry, "", client.JSON_INDENT)
if err != nil { if err != nil {
c.UI.Error("Failed to encode output data") c.UI.Error("Failed to encode output data")
return 1 return 1

View File

@ -0,0 +1,172 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package client
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"github.com/hashicorp/consul/proto-public/pbresource"
)
const (
HeaderConsulToken = "x-consul-token"
)
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 (client *GRPCClient) Apply(parsedResource *pbresource.Resource) (*pbresource.Resource, error) {
token, err := client.Config.GetToken()
if err != nil {
return nil, err
}
ctx := context.Background()
if token != "" {
ctx = metadata.AppendToOutgoingContext(ctx, HeaderConsulToken, token)
}
defer client.Conn.Close()
writeRsp, err := client.Client.Write(ctx, &pbresource.WriteRequest{Resource: parsedResource})
if err != nil {
return nil, fmt.Errorf("error writing resource: %+v", err)
}
return writeRsp.Resource, err
}
func (client *GRPCClient) Read(resourceType *pbresource.Type, resourceTenancy *pbresource.Tenancy, resourceName string, stale bool) (*pbresource.Resource, error) {
token, err := client.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(ctx, HeaderConsulToken, token)
}
defer client.Conn.Close()
readRsp, err := client.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 (client *GRPCClient) List(resourceType *pbresource.Type, resourceTenancy *pbresource.Tenancy, prefix string, stale bool) ([]*pbresource.Resource, error) {
token, err := client.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 client.Conn.Close()
listRsp, err := client.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 (client *GRPCClient) Delete(resourceType *pbresource.Type, resourceTenancy *pbresource.Tenancy, resourceName string) error {
token, err := client.Config.GetToken()
if err != nil {
return err
}
ctx := context.Background()
if token != "" {
ctx = metadata.AppendToOutgoingContext(context.Background(), HeaderConsulToken, token)
}
defer client.Conn.Close()
_, err = client.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
}
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.CertFile == ""
// both files need to be empty or both files need to be provided
if certFileEmpty != keyFileEmpty {
return fmt.Errorf("you have to provide client certificate file and key file at the same time " +
"if you intend to communicate in TLS/SSL mode")
}
}
return nil
}

View File

@ -1,65 +0,0 @@
// 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.CertFile == ""
// both files need to be empty or both files need to be provided
if certFileEmpty != keyFileEmpty {
return fmt.Errorf("you have to provide client certificate file and key file at the same time " +
"if you intend to communicate in TLS/SSL mode")
}
}
return nil
}

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import (
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/resource"
"github.com/hashicorp/consul/command/resource/client" "github.com/hashicorp/consul/command/resource/client"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
) )
@ -64,7 +63,7 @@ func (c *cmd) Run(args []string) int {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1 return 1
} }
parsedResource, err := resource.ParseResourceFromFile(c.filePath) parsedResource, err := client.ParseResourceFromFile(c.filePath)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err))
return 1 return 1
@ -80,14 +79,14 @@ func (c *cmd) Run(args []string) int {
resourceName = parsedResource.Id.Name resourceName = parsedResource.Id.Name
} else { } else {
var err error var err error
resourceType, resourceName, err = resource.GetTypeAndResourceName(args) resourceType, resourceName, err = client.GetTypeAndResourceName(args)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err))
return 1 return 1
} }
inputArgs := args[2:] inputArgs := args[2:]
err = resource.ParseInputParams(inputArgs, c.flags) err = client.ParseInputParams(inputArgs, c.flags)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err)) c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err))
return 1 return 1
@ -116,8 +115,7 @@ func (c *cmd) Run(args []string) int {
} }
// delete resource // delete resource
res := resource.ResourceGRPC{C: resourceClient} err = resourceClient.Delete(resourceType, resourceTenancy, resourceName)
err = res.Delete(resourceType, resourceTenancy, resourceName)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error deleting resource %s/%s: %v", resourceType, resourceName, err)) c.UI.Error(fmt.Sprintf("Error deleting resource %s/%s: %v", resourceType, resourceName, err))
return 1 return 1

View File

@ -1,216 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package resource
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"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/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"`
}
type Type struct {
Group string `json:"group"`
GroupVersion string `json:"groupVersion"`
Kind string `json:"kind"`
}
type ID struct {
Name string `json:"name"`
Tenancy Tenancy `json:"tenancy"`
Type Type `json:"type"`
UID string `json:"uid"`
}
func ParseResourceFromFile(filePath string) (*pbresource.Resource, error) {
return ParseResourceInput(filePath, nil)
}
func ParseResourceInput(filePath string, stdin io.Reader) (*pbresource.Resource, error) {
data, err := helpers.LoadDataSourceNoRaw(filePath, stdin)
if err != nil {
return nil, fmt.Errorf("Failed to load data: %v", err)
}
var parsedResource *pbresource.Resource
if isHCL([]byte(data)) {
parsedResource, err = resourcehcl.Unmarshal([]byte(data), consul.NewTypeRegistry())
} else {
parsedResource, err = parseJson(data)
}
if err != nil {
return nil, fmt.Errorf("Failed to decode resource from input: %v", err)
}
return parsedResource, nil
}
func ParseInputParams(inputArgs []string, flags *flag.FlagSet) error {
if err := flags.Parse(inputArgs); err != nil {
if !errors.Is(err, flag.ErrHelp) {
return fmt.Errorf("Failed to parse args: %v", err)
}
}
return nil
}
func GetTypeAndResourceName(args []string) (resourceType *pbresource.Type, resourceName string, e error) {
if len(args) < 2 {
return nil, "", fmt.Errorf("Must specify two arguments: resource type and resource name")
}
// it has to be resource name after the type
if strings.HasPrefix(args[1], "-") {
return nil, "", fmt.Errorf("Must provide resource name right after type")
}
resourceName = args[1]
resourceType, e = InferTypeFromResourceType(args[0])
return resourceType, resourceName, e
}
func InferTypeFromResourceType(resourceType string) (*pbresource.Type, error) {
s := strings.Split(resourceType, ".")
switch length := len(s); {
// only kind is provided
case length == 1:
kindToGVKMap := BuildKindToGVKMap()
kind := strings.ToLower(s[0])
switch len(kindToGVKMap[kind]) {
// no g.v.k is found
case 0:
return nil, fmt.Errorf("The shorthand name does not map to any existing resource type, please check `consul api-resources`")
// only one is found
case 1:
// infer gvk from resource kind
gvkSplit := strings.Split(kindToGVKMap[kind][0], ".")
return &pbresource.Type{
Group: gvkSplit[0],
GroupVersion: gvkSplit[1],
Kind: gvkSplit[2],
}, nil
// it alerts error if any conflict is found
default:
return nil, fmt.Errorf("The shorthand name has conflicts %v, please use the full name", kindToGVKMap[s[0]])
}
case length == 3:
return &pbresource.Type{
Group: s[0],
GroupVersion: s[1],
Kind: s[2],
}, nil
default:
return nil, fmt.Errorf("Must provide resource type argument with either in group.version.kind format or its shorthand name")
}
}
func BuildKindToGVKMap() map[string][]string {
// this use the local copy of registration to build map
typeRegistry := consul.NewTypeRegistry()
kindToGVKMap := map[string][]string{}
for _, r := range typeRegistry.Types() {
gvkString := fmt.Sprintf("%s.%s.%s", r.Type.Group, r.Type.GroupVersion, r.Type.Kind)
kindKey := strings.ToLower(r.Type.Kind)
if len(kindToGVKMap[kindKey]) == 0 {
kindToGVKMap[kindKey] = []string{gvkString}
} else {
kindToGVKMap[kindKey] = append(kindToGVKMap[kindKey], gvkString)
}
}
return kindToGVKMap
}
// this is an inlined variant of hcl.lexMode()
func isHCL(v []byte) bool {
var (
r rune
w int
offset int
)
for {
r, w = utf8.DecodeRune(v[offset:])
offset += w
if unicode.IsSpace(r) {
continue
}
if r == '{' {
return false
}
break
}
return true
}
func parseJson(js string) (*pbresource.Resource, error) {
parsedResource := new(pbresource.Resource)
var outerResource OuterResource
if err := json.Unmarshal([]byte(js), &outerResource); err != nil {
return nil, err
}
if outerResource.ID == nil {
return nil, fmt.Errorf("\"id\" field need to be provided")
}
typ := pbresource.Type{
Kind: outerResource.ID.Type.Kind,
Group: outerResource.ID.Type.Group,
GroupVersion: outerResource.ID.Type.GroupVersion,
}
reg, ok := consul.NewTypeRegistry().Resolve(&typ)
if !ok {
return nil, fmt.Errorf("invalid type %v", parsedResource)
}
data := reg.Proto.ProtoReflect().New().Interface()
anyProtoMsg, err := anypb.New(data)
if err != nil {
return nil, err
}
outerResource.Data["@type"] = anyProtoMsg.TypeUrl
marshal, err := json.Marshal(outerResource)
if err != nil {
return nil, err
}
if err := protojson.Unmarshal(marshal, parsedResource); err != nil {
return nil, err
}
return parsedResource, nil
}

View File

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

View File

@ -13,7 +13,6 @@ import (
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/resource"
"github.com/hashicorp/consul/command/resource/client" "github.com/hashicorp/consul/command/resource/client"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
) )
@ -68,7 +67,7 @@ func (c *cmd) Run(args []string) int {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1 return 1
} }
parsedResource, err := resource.ParseResourceFromFile(c.filePath) parsedResource, err := client.ParseResourceFromFile(c.filePath)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err))
return 1 return 1
@ -88,7 +87,7 @@ func (c *cmd) Run(args []string) int {
c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err))
return 1 return 1
} }
resourceType, err = resource.InferTypeFromResourceType(args[0]) resourceType, err = client.InferTypeFromResourceType(args[0])
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err))
return 1 return 1
@ -96,7 +95,7 @@ func (c *cmd) Run(args []string) int {
// skip resource type to parse remaining args // skip resource type to parse remaining args
inputArgs := c.flags.Args()[1:] inputArgs := c.flags.Args()[1:]
err = resource.ParseInputParams(inputArgs, c.flags) err = client.ParseInputParams(inputArgs, c.flags)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err)) c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err))
return 1 return 1
@ -125,15 +124,14 @@ func (c *cmd) Run(args []string) int {
} }
// list resource // list resource
res := resource.ResourceGRPC{C: resourceClient} entry, err := resourceClient.List(resourceType, resourceTenancy, c.prefix, c.resourceFlags.Stale())
entry, err := res.List(resourceType, resourceTenancy, c.prefix, c.resourceFlags.Stale())
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error listing resource %s/%s: %v", resourceType, c.prefix, err)) c.UI.Error(fmt.Sprintf("Error listing resource %s/%s: %v", resourceType, c.prefix, err))
return 1 return 1
} }
// display response // display response
b, err := json.MarshalIndent(entry, "", resource.JSON_INDENT) b, err := json.MarshalIndent(entry, "", client.JSON_INDENT)
if err != nil { if err != nil {
c.UI.Error("Failed to encode output data") c.UI.Error("Failed to encode output data")
return 1 return 1

View File

@ -12,7 +12,6 @@ import (
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/resource"
"github.com/hashicorp/consul/command/resource/client" "github.com/hashicorp/consul/command/resource/client"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
) )
@ -65,7 +64,7 @@ func (c *cmd) Run(args []string) int {
c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition"))
return 1 return 1
} }
parsedResource, err := resource.ParseResourceFromFile(c.filePath) parsedResource, err := client.ParseResourceFromFile(c.filePath)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err))
return 1 return 1
@ -81,14 +80,14 @@ func (c *cmd) Run(args []string) int {
resourceName = parsedResource.Id.Name resourceName = parsedResource.Id.Name
} else { } else {
var err error var err error
resourceType, resourceName, err = resource.GetTypeAndResourceName(args) resourceType, resourceName, err = client.GetTypeAndResourceName(args)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err))
return 1 return 1
} }
inputArgs := args[2:] inputArgs := args[2:]
err = resource.ParseInputParams(inputArgs, c.flags) err = client.ParseInputParams(inputArgs, c.flags)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err)) c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err))
return 1 return 1
@ -117,15 +116,14 @@ func (c *cmd) Run(args []string) int {
} }
// read resource // read resource
res := resource.ResourceGRPC{C: resourceClient} entry, err := resourceClient.Read(resourceType, resourceTenancy, resourceName, c.resourceFlags.Stale())
entry, err := res.Read(resourceType, resourceTenancy, resourceName, c.resourceFlags.Stale())
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", resourceType, resourceName, err)) c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", resourceType, resourceName, err))
return 1 return 1
} }
// display response // display response
b, err := json.MarshalIndent(entry, "", resource.JSON_INDENT) b, err := json.MarshalIndent(entry, "", client.JSON_INDENT)
if err != nil { if err != nil {
c.UI.Error("Failed to encode output data") c.UI.Error("Failed to encode output data")
return 1 return 1
@ -160,7 +158,6 @@ $ consul resource read -f resource.hcl
In resource.hcl, it could be: In resource.hcl, it could be:
ID { ID {
<<<<<<< HEAD
Type = gvk("catalog.v2beta1.Service") Type = gvk("catalog.v2beta1.Service")
Name = "card-processor" Name = "card-processor"
Tenancy { Tenancy {

View File

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

View File

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