mirror of https://github.com/status-im/consul.git
parent
2b89025eab
commit
3c5cb04b0f
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue