diff --git a/command/resource/apply/apply.go b/command/resource/apply/apply.go index 48f238a433..a435d43fc8 100644 --- a/command/resource/apply/apply.go +++ b/command/resource/apply/apply.go @@ -12,7 +12,6 @@ import ( "github.com/mitchellh/cli" - "github.com/hashicorp/consul/command/resource" "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") return 1 } - parsedResource, err := resource.ParseResourceInput(input, c.testStdin) + parsedResource, err := client.ParseResourceInput(input, c.testStdin) if err != nil { c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) return 1 @@ -83,15 +82,14 @@ func (c *cmd) Run(args []string) int { } // write resource - res := resource.ResourceGRPC{C: resourceClient} - entry, err := res.Apply(parsedResource) + entry, err := resourceClient.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) + b, err := json.MarshalIndent(entry, "", client.JSON_INDENT) if err != nil { c.UI.Error("Failed to encode output data") return 1 diff --git a/command/resource/client/client.go b/command/resource/client/client.go new file mode 100644 index 0000000000..909e0a0e2c --- /dev/null +++ b/command/resource/client/client.go @@ -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 +} diff --git a/command/resource/client/grpc-client_test.go b/command/resource/client/client_test.go similarity index 100% rename from command/resource/client/grpc-client_test.go rename to command/resource/client/client_test.go diff --git a/command/resource/client/grpc-config.go b/command/resource/client/config.go similarity index 100% rename from command/resource/client/grpc-config.go rename to command/resource/client/config.go diff --git a/command/resource/client/grpc-config_test.go b/command/resource/client/config_test.go similarity index 100% rename from command/resource/client/grpc-config_test.go rename to command/resource/client/config_test.go diff --git a/command/resource/client/grpc-client.go b/command/resource/client/grpc-client.go deleted file mode 100644 index 338f26d59e..0000000000 --- a/command/resource/client/grpc-client.go +++ /dev/null @@ -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 -} diff --git a/command/resource/client/helper.go b/command/resource/client/helper.go index 186691c50c..7d3245773a 100644 --- a/command/resource/client/helper.go +++ b/command/resource/client/helper.go @@ -5,13 +5,28 @@ package client import ( "crypto/tls" + "encoding/json" + "errors" + "flag" "fmt" + "io" "strconv" "strings" + "unicode" + "unicode/utf8" "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 func SetupTLSConfig(c *GRPCConfig) (*tls.Config, error) { tlsConfig := &tls.Config{ @@ -91,3 +106,194 @@ func (t *TValue[T]) Merge(onto *T) error { } 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 +} diff --git a/command/resource/client/helper_test.go b/command/resource/client/helper_test.go index fbedc6552d..e12e185dee 100644 --- a/command/resource/client/helper_test.go +++ b/command/resource/client/helper_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTValue(t *testing.T) { @@ -68,3 +69,27 @@ func TestTValue(t *testing.T) { 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) + } + + }) + } +} diff --git a/command/resource/client/grpc-resource-flags.go b/command/resource/client/resource-flags.go similarity index 100% rename from command/resource/client/grpc-resource-flags.go rename to command/resource/client/resource-flags.go diff --git a/command/resource/delete/delete.go b/command/resource/delete/delete.go index bf371f7af5..d79e8c6105 100644 --- a/command/resource/delete/delete.go +++ b/command/resource/delete/delete.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -64,7 +63,7 @@ func (c *cmd) Run(args []string) int { c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) return 1 } - parsedResource, err := resource.ParseResourceFromFile(c.filePath) + parsedResource, err := client.ParseResourceFromFile(c.filePath) if err != nil { c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) return 1 @@ -80,14 +79,14 @@ func (c *cmd) Run(args []string) int { resourceName = parsedResource.Id.Name } else { var err error - resourceType, resourceName, err = resource.GetTypeAndResourceName(args) + resourceType, resourceName, err = client.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) + err = client.ParseInputParams(inputArgs, c.flags) if err != nil { c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err)) return 1 @@ -116,8 +115,7 @@ func (c *cmd) Run(args []string) int { } // delete resource - res := resource.ResourceGRPC{C: resourceClient} - err = res.Delete(resourceType, resourceTenancy, resourceName) + err = resourceClient.Delete(resourceType, resourceTenancy, resourceName) if err != nil { c.UI.Error(fmt.Sprintf("Error deleting resource %s/%s: %v", resourceType, resourceName, err)) return 1 diff --git a/command/resource/helper.go b/command/resource/helper.go deleted file mode 100644 index d541cdd125..0000000000 --- a/command/resource/helper.go +++ /dev/null @@ -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 -} diff --git a/command/resource/helper_test.go b/command/resource/helper_test.go deleted file mode 100644 index 1b3d6cbb29..0000000000 --- a/command/resource/helper_test.go +++ /dev/null @@ -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) - } - - }) - } -} diff --git a/command/resource/list/list.go b/command/resource/list/list.go index 9d07d9575a..c29d462fca 100644 --- a/command/resource/list/list.go +++ b/command/resource/list/list.go @@ -13,7 +13,6 @@ import ( "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" ) @@ -68,7 +67,7 @@ func (c *cmd) Run(args []string) int { c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) return 1 } - parsedResource, err := resource.ParseResourceFromFile(c.filePath) + parsedResource, err := client.ParseResourceFromFile(c.filePath) if err != nil { c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) return 1 @@ -88,7 +87,7 @@ func (c *cmd) Run(args []string) int { c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) return 1 } - resourceType, err = resource.InferTypeFromResourceType(args[0]) + resourceType, err = client.InferTypeFromResourceType(args[0]) if err != nil { c.UI.Error(fmt.Sprintf("Incorrect argument format: %s", err)) return 1 @@ -96,7 +95,7 @@ func (c *cmd) Run(args []string) int { // skip resource type to parse remaining args inputArgs := c.flags.Args()[1:] - err = resource.ParseInputParams(inputArgs, c.flags) + err = client.ParseInputParams(inputArgs, c.flags) if err != nil { c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err)) return 1 @@ -125,15 +124,14 @@ func (c *cmd) Run(args []string) int { } // list resource - res := resource.ResourceGRPC{C: resourceClient} - entry, err := res.List(resourceType, resourceTenancy, c.prefix, c.resourceFlags.Stale()) + entry, err := resourceClient.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) + b, err := json.MarshalIndent(entry, "", client.JSON_INDENT) if err != nil { c.UI.Error("Failed to encode output data") return 1 diff --git a/command/resource/read/read.go b/command/resource/read/read.go index 0eb7a945bf..e00c332987 100644 --- a/command/resource/read/read.go +++ b/command/resource/read/read.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -65,7 +64,7 @@ func (c *cmd) Run(args []string) int { c.UI.Error(fmt.Sprintf("Please provide an input file with resource definition")) return 1 } - parsedResource, err := resource.ParseResourceFromFile(c.filePath) + parsedResource, err := client.ParseResourceFromFile(c.filePath) if err != nil { c.UI.Error(fmt.Sprintf("Failed to decode resource from input file: %v", err)) return 1 @@ -81,14 +80,14 @@ func (c *cmd) Run(args []string) int { resourceName = parsedResource.Id.Name } else { var err error - resourceType, resourceName, err = resource.GetTypeAndResourceName(args) + resourceType, resourceName, err = client.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) + err = client.ParseInputParams(inputArgs, c.flags) if err != nil { c.UI.Error(fmt.Sprintf("Error parsing input arguments: %v", err)) return 1 @@ -117,15 +116,14 @@ func (c *cmd) Run(args []string) int { } // read resource - res := resource.ResourceGRPC{C: resourceClient} - entry, err := res.Read(resourceType, resourceTenancy, resourceName, c.resourceFlags.Stale()) + entry, err := resourceClient.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) + b, err := json.MarshalIndent(entry, "", client.JSON_INDENT) if err != nil { c.UI.Error("Failed to encode output data") return 1 @@ -160,7 +158,6 @@ $ consul resource read -f resource.hcl In resource.hcl, it could be: ID { -<<<<<<< HEAD Type = gvk("catalog.v2beta1.Service") Name = "card-processor" Tenancy { diff --git a/command/resource/resource-grpc.go b/command/resource/resource-grpc.go deleted file mode 100644 index 94d7314597..0000000000 --- a/command/resource/resource-grpc.go +++ /dev/null @@ -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 -} diff --git a/command/resource/resource.go b/command/resource/resource.go index e7a74e498b..381351cc59 100644 --- a/command/resource/resource.go +++ b/command/resource/resource.go @@ -47,6 +47,10 @@ List resources by type: $ consul resource list [type] -partition= -namespace= +Delete a resource: + +$ consul resource delete [type] [name] -partition= -namespace= -consistent= -json + Run consul resource -h