mirror of https://github.com/status-im/consul.git
parent
2b89025eab
commit
3c5cb04b0f
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
"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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
||||
Delete a resource:
|
||||
|
||||
$ consul resource delete [type] [name] -partition=<default> -namespace=<default> -consistent=<false> -json
|
||||
|
||||
Run
|
||||
|
||||
consul resource <subcommand> -h
|
||||
|
|
Loading…
Reference in New Issue