2023-09-11 14:06:00 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
|
|
|
|
package resource
|
|
|
|
|
|
|
|
import (
|
2023-09-22 20:51:18 +00:00
|
|
|
"encoding/json"
|
2023-09-11 14:06:00 +00:00
|
|
|
"errors"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
2023-10-13 13:24:16 +00:00
|
|
|
"io"
|
2023-09-22 20:32:08 +00:00
|
|
|
"net/http"
|
2023-09-13 18:48:40 +00:00
|
|
|
"strings"
|
2023-10-11 16:37:50 +00:00
|
|
|
"unicode"
|
|
|
|
"unicode/utf8"
|
|
|
|
|
|
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
|
|
"google.golang.org/protobuf/types/known/anypb"
|
2023-09-11 14:06:00 +00:00
|
|
|
|
|
|
|
"github.com/hashicorp/consul/agent/consul"
|
|
|
|
"github.com/hashicorp/consul/command/helpers"
|
2023-09-22 20:32:08 +00:00
|
|
|
"github.com/hashicorp/consul/command/resource/client"
|
2023-09-11 14:06:00 +00:00
|
|
|
"github.com/hashicorp/consul/internal/resourcehcl"
|
|
|
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
|
|
|
)
|
|
|
|
|
2023-09-22 20:51:18 +00:00
|
|
|
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 {
|
|
|
|
Namespace string `json:"namespace"`
|
|
|
|
Partition string `json:"partition"`
|
|
|
|
PeerName string `json:"peerName"`
|
|
|
|
}
|
2023-10-11 16:37:50 +00:00
|
|
|
|
2023-09-22 20:51:18 +00:00
|
|
|
type Type struct {
|
|
|
|
Group string `json:"group"`
|
|
|
|
GroupVersion string `json:"groupVersion"`
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
}
|
2023-10-11 16:37:50 +00:00
|
|
|
|
2023-09-22 20:51:18 +00:00
|
|
|
type ID struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
Tenancy Tenancy `json:"tenancy"`
|
|
|
|
Type Type `json:"type"`
|
|
|
|
UID string `json:"uid"`
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-09-11 14:06:00 +00:00
|
|
|
func ParseResourceFromFile(filePath string) (*pbresource.Resource, error) {
|
2023-10-24 15:53:51 +00:00
|
|
|
return ParseResourceInput(filePath, nil)
|
2023-09-11 14:06:00 +00:00
|
|
|
}
|
|
|
|
|
2023-10-11 16:37:50 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2023-10-13 13:24:16 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-09-11 14:06:00 +00:00
|
|
|
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
|
|
|
|
}
|
2023-09-13 18:48:40 +00:00
|
|
|
|
2023-09-22 20:32:08 +00:00
|
|
|
func GetTypeAndResourceName(args []string) (gvk *GVK, resourceName string, e error) {
|
2023-10-24 15:53:51 +00:00
|
|
|
if len(args) < 2 {
|
|
|
|
return nil, "", fmt.Errorf("Must specify two arguments: resource type and resource name")
|
|
|
|
}
|
2023-09-13 18:48:40 +00:00
|
|
|
// 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")
|
|
|
|
}
|
2023-10-24 15:53:51 +00:00
|
|
|
resourceName = args[1]
|
2023-09-13 18:48:40 +00:00
|
|
|
|
2023-10-24 15:53:51 +00:00
|
|
|
gvk, e = inferGVKFromResourceType(args[0])
|
2023-09-13 18:48:40 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
2023-09-22 20:32:08 +00:00
|
|
|
|
|
|
|
type Resource struct {
|
|
|
|
C *client.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
type GVK struct {
|
|
|
|
Group string
|
|
|
|
Version string
|
|
|
|
Kind string
|
|
|
|
}
|
|
|
|
|
|
|
|
type WriteRequest struct {
|
|
|
|
Metadata map[string]string `json:"metadata"`
|
|
|
|
Data map[string]any `json:"data"`
|
|
|
|
Owner *pbresource.ID `json:"owner"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type ListResponse struct {
|
|
|
|
Resources []map[string]interface{} `json:"resources"`
|
|
|
|
}
|
|
|
|
|
2023-11-21 16:29:00 +00:00
|
|
|
func (gvk *GVK) String() string {
|
|
|
|
return fmt.Sprintf("%s.%s.%s", gvk.Group, gvk.Version, gvk.Kind)
|
|
|
|
}
|
|
|
|
|
2023-09-22 20:32:08 +00:00
|
|
|
func (resource *Resource) Read(gvk *GVK, resourceName string, q *client.QueryOptions) (map[string]interface{}, error) {
|
|
|
|
r := resource.C.NewRequest("GET", strings.ToLower(fmt.Sprintf("/api/%s/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind, resourceName)))
|
|
|
|
r.SetQueryOptions(q)
|
|
|
|
_, resp, err := resource.C.DoRequest(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer client.CloseResponseBody(resp)
|
|
|
|
if err := client.RequireOK(resp); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var out map[string]interface{}
|
|
|
|
if err := client.DecodeBody(resp, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (resource *Resource) Delete(gvk *GVK, resourceName string, q *client.QueryOptions) error {
|
|
|
|
r := resource.C.NewRequest("DELETE", strings.ToLower(fmt.Sprintf("/api/%s/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind, resourceName)))
|
|
|
|
r.SetQueryOptions(q)
|
|
|
|
_, resp, err := resource.C.DoRequest(r)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer client.CloseResponseBody(resp)
|
|
|
|
if err := client.RequireHttpCodes(resp, http.StatusNoContent); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (resource *Resource) Apply(gvk *GVK, resourceName string, q *client.QueryOptions, payload *WriteRequest) (*map[string]interface{}, error) {
|
|
|
|
url := strings.ToLower(fmt.Sprintf("/api/%s/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind, resourceName))
|
|
|
|
|
|
|
|
r := resource.C.NewRequest("PUT", url)
|
|
|
|
r.SetQueryOptions(q)
|
|
|
|
r.Obj = payload
|
|
|
|
_, resp, err := resource.C.DoRequest(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer client.CloseResponseBody(resp)
|
|
|
|
if err := client.RequireOK(resp); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var out map[string]interface{}
|
|
|
|
|
|
|
|
if err := client.DecodeBody(resp, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &out, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (resource *Resource) List(gvk *GVK, q *client.QueryOptions) (*ListResponse, error) {
|
|
|
|
r := resource.C.NewRequest("GET", strings.ToLower(fmt.Sprintf("/api/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind)))
|
|
|
|
r.SetQueryOptions(q)
|
|
|
|
_, resp, err := resource.C.DoRequest(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer client.CloseResponseBody(resp)
|
|
|
|
if err := client.RequireOK(resp); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var out *ListResponse
|
|
|
|
if err := client.DecodeBody(resp, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|
2023-10-24 15:53:51 +00:00
|
|
|
|
|
|
|
func inferGVKFromResourceType(resourceType string) (*GVK, 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 &GVK{
|
|
|
|
Group: gvkSplit[0],
|
|
|
|
Version: 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 &GVK{
|
|
|
|
Group: s[0],
|
|
|
|
Version: s[1],
|
|
|
|
Kind: s[2],
|
|
|
|
}, nil
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("Must provide resource type argument with either in group.verion.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
|
|
|
|
}
|