diff --git a/api/go.mod b/api/go.mod index 6adfb1c628..0c6c4ee069 100644 --- a/api/go.mod +++ b/api/go.mod @@ -2,14 +2,10 @@ module github.com/hashicorp/consul/api go 1.19 -replace ( - github.com/hashicorp/consul/proto-public => ../proto-public - github.com/hashicorp/consul/sdk => ../sdk -) +replace github.com/hashicorp/consul/sdk => ../sdk require ( github.com/google/go-cmp v0.5.9 - github.com/hashicorp/consul/proto-public v0.4.1 github.com/hashicorp/consul/sdk v0.14.1 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-hclog v1.5.0 @@ -25,7 +21,6 @@ require ( github.com/armon/go-metrics v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fatih/color v1.14.1 // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.0.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect @@ -47,9 +42,5 @@ require ( golang.org/x/net v0.13.0 // indirect golang.org/x/sync v0.2.0 // indirect golang.org/x/sys v0.11.0 // indirect - golang.org/x/text v0.11.0 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.55.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/api/go.sum b/api/go.sum index d2b6015ad2..6411c8af54 100644 --- a/api/go.sum +++ b/api/go.sum @@ -35,15 +35,11 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -222,20 +218,10 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/resource.go b/api/resource.go deleted file mode 100644 index ba9b21e6b7..0000000000 --- a/api/resource.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package api - -import ( - "fmt" - "net/http" - "strings" - - "github.com/hashicorp/consul/proto-public/pbresource" -) - -type Resource struct { - c *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 WriteResponse struct { - Metadata map[string]string `json:"metadata"` - Data map[string]any `json:"data"` - Owner *pbresource.ID `json:"owner,omitempty"` - ID *pbresource.ID `json:"id"` - Version string `json:"version"` - Generation string `json:"generation"` - Status map[string]any `json:"status"` -} - -type ListResponse struct { - Resources []WriteResponse `json:"resources"` -} - -// Config returns a handle to the Config endpoints -func (c *Client) Resource() *Resource { - return &Resource{c} -} - -func (resource *Resource) Read(gvk *GVK, resourceName string, q *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 closeResponseBody(resp) - if err := requireOK(resp); err != nil { - return nil, err - } - - var out map[string]interface{} - if err := decodeBody(resp, &out); err != nil { - return nil, err - } - - return out, nil -} - -func (resource *Resource) Delete(gvk *GVK, resourceName string, q *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 closeResponseBody(resp) - if err := requireHttpCodes(resp, http.StatusNoContent); err != nil { - return err - } - return nil -} - -func (resource *Resource) Apply(gvk *GVK, resourceName string, q *QueryOptions, payload *WriteRequest) (*WriteResponse, *WriteMeta, 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 - rtt, resp, err := resource.c.doRequest(r) - if err != nil { - return nil, nil, err - } - defer closeResponseBody(resp) - if err := requireOK(resp); err != nil { - return nil, nil, err - } - - wm := &WriteMeta{} - wm.RequestTime = rtt - - var out *WriteResponse - if err := decodeBody(resp, &out); err != nil { - return nil, nil, err - } - return out, wm, nil -} - -func (resource *Resource) List(gvk *GVK, q *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 closeResponseBody(resp) - if err := requireOK(resp); err != nil { - return nil, err - } - - var out *ListResponse - if err := decodeBody(resp, &out); err != nil { - return nil, err - } - - return out, nil -} diff --git a/command/resource/apply/apply.go b/command/resource/apply/apply.go index 38ea9c5125..9f0b8e4581 100644 --- a/command/resource/apply/apply.go +++ b/command/resource/apply/apply.go @@ -12,11 +12,10 @@ import ( "github.com/mitchellh/cli" "google.golang.org/protobuf/encoding/protojson" - "github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/resource" - "github.com/hashicorp/consul/internal/resourcehcl" + "github.com/hashicorp/consul/command/resource/client" "github.com/hashicorp/consul/proto-public/pbresource" ) @@ -45,7 +44,7 @@ func (c *cmd) init() { c.help = flags.Usage(help, c.flags) } -func makeWriteRequest(parsedResource *pbresource.Resource) (payload *api.WriteRequest, error error) { +func makeWriteRequest(parsedResource *pbresource.Resource) (payload *resource.WriteRequest, error error) { // The parsed hcl file has data field in proto message format anypb.Any // Converting to json format requires us to fisrt marshal it then unmarshal it data, err := protojson.Marshal(parsedResource.Data) @@ -60,7 +59,7 @@ func makeWriteRequest(parsedResource *pbresource.Resource) (payload *api.WriteRe } delete(resourceData, "@type") - return &api.WriteRequest{ + return &resource.WriteRequest{ Data: resourceData, Metadata: parsedResource.GetMetadata(), Owner: parsedResource.GetOwner(), @@ -94,20 +93,25 @@ func (c *cmd) Run(args []string) int { return 1 } - client, err := c.http.APIClient() + config := api.DefaultConfig() + + c.http.MergeOntoConfig(config) + resourceClient, err := client.NewClient(config) if err != nil { c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) return 1 } - opts := &api.QueryOptions{ + res := resource.Resource{C: resourceClient} + + opts := &client.QueryOptions{ Namespace: parsedResource.Id.Tenancy.GetNamespace(), Partition: parsedResource.Id.Tenancy.GetPartition(), Peer: parsedResource.Id.Tenancy.GetPeerName(), Token: c.http.Token(), } - gvk := &api.GVK{ + gvk := &resource.GVK{ Group: parsedResource.Id.Type.GetGroup(), Version: parsedResource.Id.Type.GetGroupVersion(), Kind: parsedResource.Id.Type.GetKind(), @@ -119,7 +123,7 @@ func (c *cmd) Run(args []string) int { return 1 } - entry, _, err := client.Resource().Apply(gvk, parsedResource.Id.GetName(), opts, writeRequest) + entry, err := res.Apply(gvk, parsedResource.Id.GetName(), opts, writeRequest) if err != nil { c.UI.Error(fmt.Sprintf("Error writing resource %s/%s: %v", gvk, parsedResource.Id.GetName(), err)) return 1 @@ -136,17 +140,6 @@ func (c *cmd) Run(args []string) int { return 0 } -func parseResource(data string) (resource *pbresource.Resource, e error) { - // parse the data - raw := []byte(data) - resource, err := resourcehcl.Unmarshal(raw, consul.NewTypeRegistry()) - if err != nil { - return nil, fmt.Errorf("Failed to decode resource from input file: %v", err) - } - - return resource, nil -} - func (c *cmd) Synopsis() string { return synopsis } diff --git a/command/resource/client/client.go b/command/resource/client/client.go new file mode 100644 index 0000000000..e01cb6275c --- /dev/null +++ b/command/resource/client/client.go @@ -0,0 +1,1072 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package client + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-rootcerts" +) + +// NOTE: This client is copied from the api module to temporarily facilitate the resource cli commands + +const ( + // HTTPAddrEnvName defines an environment variable name which sets + // the HTTP address if there is no -http-addr specified. + HTTPAddrEnvName = "CONSUL_HTTP_ADDR" + + // HTTPTokenEnvName defines an environment variable name which sets + // the HTTP token. + HTTPTokenEnvName = "CONSUL_HTTP_TOKEN" + + // HTTPTokenFileEnvName defines an environment variable name which sets + // the HTTP token file. + HTTPTokenFileEnvName = "CONSUL_HTTP_TOKEN_FILE" + + // HTTPAuthEnvName defines an environment variable name which sets + // the HTTP authentication header. + HTTPAuthEnvName = "CONSUL_HTTP_AUTH" + + // HTTPSSLEnvName defines an environment variable name which sets + // whether or not to use HTTPS. + HTTPSSLEnvName = "CONSUL_HTTP_SSL" + + // HTTPCAFile defines an environment variable name which sets the + // CA file to use for talking to Consul over TLS. + HTTPCAFile = "CONSUL_CACERT" + + // HTTPCAPath defines an environment variable name which sets the + // path to a directory of CA certs to use for talking to Consul over TLS. + HTTPCAPath = "CONSUL_CAPATH" + + // HTTPClientCert defines an environment variable name which sets the + // client cert file to use for talking to Consul over TLS. + HTTPClientCert = "CONSUL_CLIENT_CERT" + + // HTTPClientKey defines an environment variable name which sets the + // client key file to use for talking to Consul over TLS. + HTTPClientKey = "CONSUL_CLIENT_KEY" + + // HTTPTLSServerName defines an environment variable name which sets the + // server name to use as the SNI host when connecting via TLS + HTTPTLSServerName = "CONSUL_TLS_SERVER_NAME" + + // HTTPSSLVerifyEnvName defines an environment variable name which sets + // whether or not to disable certificate checking. + HTTPSSLVerifyEnvName = "CONSUL_HTTP_SSL_VERIFY" + + // GRPCAddrEnvName defines an environment variable name which sets the gRPC + // address for consul connect envoy. Note this isn't actually used by the api + // client in this package but is defined here for consistency with all the + // other ENV names we use. + GRPCAddrEnvName = "CONSUL_GRPC_ADDR" + + // GRPCCAFileEnvName defines an environment variable name which sets the + // CA file to use for talking to Consul gRPC over TLS. + GRPCCAFileEnvName = "CONSUL_GRPC_CACERT" + + // GRPCCAPathEnvName defines an environment variable name which sets the + // path to a directory of CA certs to use for talking to Consul gRPC over TLS. + GRPCCAPathEnvName = "CONSUL_GRPC_CAPATH" + + // HTTPNamespaceEnvVar defines an environment variable name which sets + // the HTTP Namespace to be used by default. This can still be overridden. + HTTPNamespaceEnvName = "CONSUL_NAMESPACE" + + // HTTPPartitionEnvName defines an environment variable name which sets + // the HTTP Partition to be used by default. This can still be overridden. + HTTPPartitionEnvName = "CONSUL_PARTITION" + + // QueryBackendStreaming Query backend of type streaming + QueryBackendStreaming = "streaming" + + // QueryBackendBlockingQuery Query backend of type blocking query + QueryBackendBlockingQuery = "blocking-query" +) + +type StatusError struct { + Code int + Body string +} + +func (e StatusError) Error() string { + return fmt.Sprintf("Unexpected response code: %d (%s)", e.Code, e.Body) +} + +// QueryOptions are used to parameterize a query +type QueryOptions struct { + // Namespace overrides the `default` namespace + // Note: Namespaces are available only in Consul Enterprise + Namespace string + + // Partition overrides the `default` partition + // Note: Partitions are available only in Consul Enterprise + Partition string + + // Providing a datacenter overwrites the DC provided + // by the Config + Datacenter string + + // Providing a peer name in the query option + Peer string + + // AllowStale allows any Consul server (non-leader) to service + // a read. This allows for lower latency and higher throughput + AllowStale bool + + // RequireConsistent forces the read to be fully consistent. + // This is more expensive but prevents ever performing a stale + // read. + RequireConsistent bool + + // UseCache requests that the agent cache results locally. See + // https://www.consul.io/api/features/caching.html for more details on the + // semantics. + UseCache bool + + // MaxAge limits how old a cached value will be returned if UseCache is true. + // If there is a cached response that is older than the MaxAge, it is treated + // as a cache miss and a new fetch invoked. If the fetch fails, the error is + // returned. Clients that wish to allow for stale results on error can set + // StaleIfError to a longer duration to change this behavior. It is ignored + // if the endpoint supports background refresh caching. See + // https://www.consul.io/api/features/caching.html for more details. + MaxAge time.Duration + + // StaleIfError specifies how stale the client will accept a cached response + // if the servers are unavailable to fetch a fresh one. Only makes sense when + // UseCache is true and MaxAge is set to a lower, non-zero value. It is + // ignored if the endpoint supports background refresh caching. See + // https://www.consul.io/api/features/caching.html for more details. + StaleIfError time.Duration + + // WaitIndex is used to enable a blocking query. Waits + // until the timeout or the next index is reached + WaitIndex uint64 + + // WaitHash is used by some endpoints instead of WaitIndex to perform blocking + // on state based on a hash of the response rather than a monotonic index. + // This is required when the state being blocked on is not stored in Raft, for + // example agent-local proxy configuration. + WaitHash string + + // WaitTime is used to bound the duration of a wait. + // Defaults to that of the Config, but can be overridden. + WaitTime time.Duration + + // Token is used to provide a per-request ACL token + // which overrides the agent's default token. + Token string + + // Near is used to provide a node name that will sort the results + // in ascending order based on the estimated round trip time from + // that node. Setting this to "_agent" will use the agent's node + // for the sort. + Near string + + // NodeMeta is used to filter results by nodes with the given + // metadata key/value pairs. Currently, only one key/value pair can + // be provided for filtering. + NodeMeta map[string]string + + // RelayFactor is used in keyring operations to cause responses to be + // relayed back to the sender through N other random nodes. Must be + // a value from 0 to 5 (inclusive). + RelayFactor uint8 + + // LocalOnly is used in keyring list operation to force the keyring + // query to only hit local servers (no WAN traffic). + LocalOnly bool + + // Connect filters prepared query execution to only include Connect-capable + // services. This currently affects prepared query execution. + Connect bool + + // ctx is an optional context pass through to the underlying HTTP + // request layer. Use Context() and WithContext() to manage this. + ctx context.Context + + // Filter requests filtering data prior to it being returned. The string + // is a go-bexpr compatible expression. + Filter string + + // MergeCentralConfig returns a service definition merged with the + // proxy-defaults/global and service-defaults/:service config entries. + // This can be used to ensure a full service definition is returned in the response + // especially when the service might not be written into the catalog that way. + MergeCentralConfig bool + + // Global is used to request information from all datacenters. Currently only + // used for operator usage requests. + Global bool +} + +func (o *QueryOptions) Context() context.Context { + if o != nil && o.ctx != nil { + return o.ctx + } + return context.Background() +} + +func (o *QueryOptions) WithContext(ctx context.Context) *QueryOptions { + o2 := new(QueryOptions) + if o != nil { + *o2 = *o + } + o2.ctx = ctx + return o2 +} + +// WriteOptions are used to parameterize a write +type WriteOptions struct { + // Namespace overrides the `default` namespace + // Note: Namespaces are available only in Consul Enterprise + Namespace string + + // Partition overrides the `default` partition + // Note: Partitions are available only in Consul Enterprise + Partition string + + // Providing a datacenter overwrites the DC provided + // by the Config + Datacenter string + + // Token is used to provide a per-request ACL token + // which overrides the agent's default token. + Token string + + // RelayFactor is used in keyring operations to cause responses to be + // relayed back to the sender through N other random nodes. Must be + // a value from 0 to 5 (inclusive). + RelayFactor uint8 + + // ctx is an optional context pass through to the underlying HTTP + // request layer. Use Context() and WithContext() to manage this. + ctx context.Context +} + +func (o *WriteOptions) Context() context.Context { + if o != nil && o.ctx != nil { + return o.ctx + } + return context.Background() +} + +func (o *WriteOptions) WithContext(ctx context.Context) *WriteOptions { + o2 := new(WriteOptions) + if o != nil { + *o2 = *o + } + o2.ctx = ctx + return o2 +} + +// QueryMeta is used to return meta data about a query +type QueryMeta struct { + // LastIndex. This can be used as a WaitIndex to perform + // a blocking query + LastIndex uint64 + + // LastContentHash. This can be used as a WaitHash to perform a blocking query + // for endpoints that support hash-based blocking. Endpoints that do not + // support it will return an empty hash. + LastContentHash string + + // Time of last contact from the leader for the + // server servicing the request + LastContact time.Duration + + // Is there a known leader + KnownLeader bool + + // How long did the request take + RequestTime time.Duration + + // Is address translation enabled for HTTP responses on this agent + AddressTranslationEnabled bool + + // CacheHit is true if the result was served from agent-local cache. + CacheHit bool + + // CacheAge is set if request was ?cached and indicates how stale the cached + // response is. + CacheAge time.Duration + + // QueryBackend represent which backend served the request. + QueryBackend string + + // DefaultACLPolicy is used to control the ACL interaction when there is no + // defined policy. This can be "allow" which means ACLs are used to + // deny-list, or "deny" which means ACLs are allow-lists. + DefaultACLPolicy string + + // ResultsFilteredByACLs is true when some of the query's results were + // filtered out by enforcing ACLs. It may be false because nothing was + // removed, or because the endpoint does not yet support this flag. + ResultsFilteredByACLs bool +} + +// WriteMeta is used to return meta data about a write +type WriteMeta struct { + // How long did the request take + RequestTime time.Duration +} + +// HttpBasicAuth is used to authenticate http client with HTTP Basic Authentication +type HttpBasicAuth struct { + // Username to use for HTTP Basic Authentication + Username string + + // Password to use for HTTP Basic Authentication + Password string +} + +// Config is used to configure the creation of a client +type Config struct { + // Address is the address of the Consul server + Address string + + // Scheme is the URI scheme for the Consul server + Scheme string + + // Prefix for URIs for when consul is behind an API gateway (reverse + // proxy). The API gateway must strip off the PathPrefix before + // passing the request onto consul. + PathPrefix string + + // Datacenter to use. If not provided, the default agent datacenter is used. + Datacenter string + + // Transport is the Transport to use for the http client. + Transport *http.Transport + + // HttpClient is the client to use. Default will be + // used if not provided. + HttpClient *http.Client + + // HttpAuth is the auth info to use for http access. + HttpAuth *HttpBasicAuth + + // WaitTime limits how long a Watch will block. If not provided, + // the agent default values will be used. + WaitTime time.Duration + + // Token is used to provide a per-request ACL token + // which overrides the agent's default token. + Token string + + // TokenFile is a file containing the current token to use for this client. + // If provided it is read once at startup and never again. + TokenFile string + + // Namespace is the name of the namespace to send along for the request + // when no other Namespace is present in the QueryOptions + Namespace string + + // Partition is the name of the partition to send along for the request + // when no other Partition is present in the QueryOptions + Partition string + + TLSConfig TLSConfig +} + +// TLSConfig is used to generate a TLSClientConfig that's useful for talking to +// Consul using TLS. +type TLSConfig struct { + // Address is the optional address of the Consul server. The port, if any + // will be removed from here and this will be set to the ServerName of the + // resulting config. + Address string + + // CAFile is the optional path to the CA certificate used for Consul + // communication, defaults to the system bundle if not specified. + CAFile string + + // CAPath is the optional path to a directory of CA certificates to use for + // Consul communication, defaults to the system bundle if not specified. + CAPath string + + // CAPem is the optional PEM-encoded CA certificate used for Consul + // communication, defaults to the system bundle if not specified. + CAPem []byte + + // CertFile is the optional path to the certificate for Consul + // communication. If this is set then you need to also set KeyFile. + CertFile string + + // CertPEM is the optional PEM-encoded certificate for Consul + // communication. If this is set then you need to also set KeyPEM. + CertPEM []byte + + // KeyFile is the optional path to the private key for Consul communication. + // If this is set then you need to also set CertFile. + KeyFile string + + // KeyPEM is the optional PEM-encoded private key for Consul communication. + // If this is set then you need to also set CertPEM. + KeyPEM []byte + + // InsecureSkipVerify if set to true will disable TLS host verification. + InsecureSkipVerify bool +} + +// DefaultConfig returns a default configuration for the client. By default this +// will pool and reuse idle connections to Consul. If you have a long-lived +// client object, this is the desired behavior and should make the most efficient +// use of the connections to Consul. If you don't reuse a client object, which +// is not recommended, then you may notice idle connections building up over +// time. To avoid this, use the DefaultNonPooledConfig() instead. +func DefaultConfig() *Config { + return defaultConfig(nil, cleanhttp.DefaultPooledTransport) +} + +// DefaultConfigWithLogger returns a default configuration for the client. It +// is exactly the same as DefaultConfig, but allows for a pre-configured logger +// object to be passed through. +func DefaultConfigWithLogger(logger hclog.Logger) *Config { + return defaultConfig(logger, cleanhttp.DefaultPooledTransport) +} + +// DefaultNonPooledConfig returns a default configuration for the client which +// does not pool connections. This isn't a recommended configuration because it +// will reconnect to Consul on every request, but this is useful to avoid the +// accumulation of idle connections if you make many client objects during the +// lifetime of your application. +func DefaultNonPooledConfig() *Config { + return defaultConfig(nil, cleanhttp.DefaultTransport) +} + +// defaultConfig returns the default configuration for the client, using the +// given function to make the transport. +func defaultConfig(logger hclog.Logger, transportFn func() *http.Transport) *Config { + if logger == nil { + logger = hclog.New(&hclog.LoggerOptions{ + Name: "consul-api", + }) + } + + config := &Config{ + Address: "127.0.0.1:8500", + Scheme: "http", + Transport: transportFn(), + } + + if addr := os.Getenv(HTTPAddrEnvName); addr != "" { + config.Address = addr + } + + if tokenFile := os.Getenv(HTTPTokenFileEnvName); tokenFile != "" { + config.TokenFile = tokenFile + } + + if token := os.Getenv(HTTPTokenEnvName); token != "" { + config.Token = token + } + + if auth := os.Getenv(HTTPAuthEnvName); auth != "" { + var username, password string + if strings.Contains(auth, ":") { + split := strings.SplitN(auth, ":", 2) + username = split[0] + password = split[1] + } else { + username = auth + } + + config.HttpAuth = &HttpBasicAuth{ + Username: username, + Password: password, + } + } + + if ssl := os.Getenv(HTTPSSLEnvName); ssl != "" { + enabled, err := strconv.ParseBool(ssl) + if err != nil { + logger.Warn(fmt.Sprintf("could not parse %s", HTTPSSLEnvName), "error", err) + } + + if enabled { + config.Scheme = "https" + } + } + + if v := os.Getenv(HTTPTLSServerName); v != "" { + config.TLSConfig.Address = v + } + if v := os.Getenv(HTTPCAFile); v != "" { + config.TLSConfig.CAFile = v + } + if v := os.Getenv(HTTPCAPath); v != "" { + config.TLSConfig.CAPath = v + } + if v := os.Getenv(HTTPClientCert); v != "" { + config.TLSConfig.CertFile = v + } + if v := os.Getenv(HTTPClientKey); v != "" { + config.TLSConfig.KeyFile = v + } + if v := os.Getenv(HTTPSSLVerifyEnvName); v != "" { + doVerify, err := strconv.ParseBool(v) + if err != nil { + logger.Warn(fmt.Sprintf("could not parse %s", HTTPSSLVerifyEnvName), "error", err) + } + if !doVerify { + config.TLSConfig.InsecureSkipVerify = true + } + } + + if v := os.Getenv(HTTPNamespaceEnvName); v != "" { + config.Namespace = v + } + + if v := os.Getenv(HTTPPartitionEnvName); v != "" { + config.Partition = v + } + + return config +} + +// TLSConfig is used to generate a TLSClientConfig that's useful for talking to +// Consul using TLS. +func SetupTLSConfig(tlsConfig *TLSConfig) (*tls.Config, error) { + tlsClientConfig := &tls.Config{ + InsecureSkipVerify: tlsConfig.InsecureSkipVerify, + } + + if tlsConfig.Address != "" { + server := tlsConfig.Address + hasPort := strings.LastIndex(server, ":") > strings.LastIndex(server, "]") + if hasPort { + var err error + server, _, err = net.SplitHostPort(server) + if err != nil { + return nil, err + } + } + tlsClientConfig.ServerName = server + } + + if len(tlsConfig.CertPEM) != 0 && len(tlsConfig.KeyPEM) != 0 { + tlsCert, err := tls.X509KeyPair(tlsConfig.CertPEM, tlsConfig.KeyPEM) + if err != nil { + return nil, err + } + tlsClientConfig.Certificates = []tls.Certificate{tlsCert} + } else if len(tlsConfig.CertPEM) != 0 || len(tlsConfig.KeyPEM) != 0 { + return nil, fmt.Errorf("both client cert and client key must be provided") + } + + if tlsConfig.CertFile != "" && tlsConfig.KeyFile != "" { + tlsCert, err := tls.LoadX509KeyPair(tlsConfig.CertFile, tlsConfig.KeyFile) + if err != nil { + return nil, err + } + tlsClientConfig.Certificates = []tls.Certificate{tlsCert} + } else if tlsConfig.CertFile != "" || tlsConfig.KeyFile != "" { + return nil, fmt.Errorf("both client cert and client key must be provided") + } + + if tlsConfig.CAFile != "" || tlsConfig.CAPath != "" || len(tlsConfig.CAPem) != 0 { + rootConfig := &rootcerts.Config{ + CAFile: tlsConfig.CAFile, + CAPath: tlsConfig.CAPath, + CACertificate: tlsConfig.CAPem, + } + if err := rootcerts.ConfigureTLS(tlsClientConfig, rootConfig); err != nil { + return nil, err + } + } + + return tlsClientConfig, nil +} + +func (c *Config) GenerateEnv() []string { + env := make([]string, 0, 10) + + env = append(env, + fmt.Sprintf("%s=%s", HTTPAddrEnvName, c.Address), + fmt.Sprintf("%s=%s", HTTPTokenEnvName, c.Token), + fmt.Sprintf("%s=%s", HTTPTokenFileEnvName, c.TokenFile), + fmt.Sprintf("%s=%t", HTTPSSLEnvName, c.Scheme == "https"), + fmt.Sprintf("%s=%s", HTTPCAFile, c.TLSConfig.CAFile), + fmt.Sprintf("%s=%s", HTTPCAPath, c.TLSConfig.CAPath), + fmt.Sprintf("%s=%s", HTTPClientCert, c.TLSConfig.CertFile), + fmt.Sprintf("%s=%s", HTTPClientKey, c.TLSConfig.KeyFile), + fmt.Sprintf("%s=%s", HTTPTLSServerName, c.TLSConfig.Address), + fmt.Sprintf("%s=%t", HTTPSSLVerifyEnvName, !c.TLSConfig.InsecureSkipVerify)) + + if c.HttpAuth != nil { + env = append(env, fmt.Sprintf("%s=%s:%s", HTTPAuthEnvName, c.HttpAuth.Username, c.HttpAuth.Password)) + } else { + env = append(env, fmt.Sprintf("%s=", HTTPAuthEnvName)) + } + + return env +} + +// Client provides a client to the Consul API +type Client struct { + modifyLock sync.RWMutex + headers http.Header + + config api.Config +} + +// Headers gets the current set of headers used for requests. This returns a +// copy; to modify it call AddHeader or SetHeaders. +func (c *Client) Headers() http.Header { + c.modifyLock.RLock() + defer c.modifyLock.RUnlock() + + if c.headers == nil { + return nil + } + + ret := make(http.Header) + for k, v := range c.headers { + for _, val := range v { + ret[k] = append(ret[k], val) + } + } + + return ret +} + +// NewClient returns a new client +func NewClient(config *api.Config) (*Client, error) { + // bootstrap the config + defConfig := api.DefaultConfig() + + if config.Address == "" { + config.Address = defConfig.Address + } + + if config.Scheme == "" { + config.Scheme = defConfig.Scheme + } + + if config.Transport == nil { + config.Transport = defConfig.Transport + } + + if config.TLSConfig.Address == "" { + config.TLSConfig.Address = defConfig.TLSConfig.Address + } + + if config.TLSConfig.CAFile == "" { + config.TLSConfig.CAFile = defConfig.TLSConfig.CAFile + } + + if config.TLSConfig.CAPath == "" { + config.TLSConfig.CAPath = defConfig.TLSConfig.CAPath + } + + if config.TLSConfig.CertFile == "" { + config.TLSConfig.CertFile = defConfig.TLSConfig.CertFile + } + + if config.TLSConfig.KeyFile == "" { + config.TLSConfig.KeyFile = defConfig.TLSConfig.KeyFile + } + + if !config.TLSConfig.InsecureSkipVerify { + config.TLSConfig.InsecureSkipVerify = defConfig.TLSConfig.InsecureSkipVerify + } + + if config.HttpClient == nil { + var err error + config.HttpClient, err = NewHttpClient(config.Transport, config.TLSConfig) + if err != nil { + return nil, err + } + } + + if config.Namespace == "" { + config.Namespace = defConfig.Namespace + } + + if config.Partition == "" { + config.Partition = defConfig.Partition + } + + parts := strings.SplitN(config.Address, "://", 2) + if len(parts) == 2 { + switch parts[0] { + case "http": + // Never revert to http if TLS was explicitly requested. + case "https": + config.Scheme = "https" + case "unix": + trans := cleanhttp.DefaultTransport() + trans.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", parts[1]) + } + httpClient, err := NewHttpClient(trans, config.TLSConfig) + if err != nil { + return nil, err + } + config.HttpClient = httpClient + default: + return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0]) + } + config.Address = parts[1] + + // separate out a reverse proxy prefix, if it is present. + // NOTE: Rewriting this code to use url.Parse() instead of + // strings.SplitN() breaks existing test cases. + switch parts[0] { + case "http", "https": + parts := strings.SplitN(parts[1], "/", 2) + if len(parts) == 2 { + config.Address = parts[0] + config.PathPrefix = "/" + parts[1] + } + } + } + + // If the TokenFile is set, always use that, even if a Token is configured. + // This is because when TokenFile is set it is read into the Token field. + // We want any derived clients to have to re-read the token file. + // The precedence of ACL token should be: + // 1. -token-file cli option + // 2. -token cli option + // 3. CONSUL_HTTP_TOKEN_FILE environment variable + // 4. CONSUL_HTTP_TOKEN environment variable + if config.TokenFile != "" && config.TokenFile != defConfig.TokenFile { + data, err := os.ReadFile(config.TokenFile) + if err != nil { + return nil, fmt.Errorf("Error loading token file %s : %s", config.TokenFile, err) + } + + if token := strings.TrimSpace(string(data)); token != "" { + config.Token = token + } + } else if config.Token != "" && defConfig.Token != config.Token { + // Fall through + } else if defConfig.TokenFile != "" { + data, err := os.ReadFile(defConfig.TokenFile) + if err != nil { + return nil, fmt.Errorf("Error loading token file %s : %s", defConfig.TokenFile, err) + } + + if token := strings.TrimSpace(string(data)); token != "" { + config.Token = token + config.TokenFile = defConfig.TokenFile + } + } else { + config.Token = defConfig.Token + } + return &Client{config: *config, headers: make(http.Header)}, nil +} + +// NewHttpClient returns an http client configured with the given Transport and TLS +// config. +func NewHttpClient(transport *http.Transport, tlsConf api.TLSConfig) (*http.Client, error) { + client := &http.Client{ + Transport: transport, + } + + // TODO (slackpad) - Once we get some run time on the HTTP/2 support we + // should turn it on by default if TLS is enabled. We would basically + // just need to call http2.ConfigureTransport(transport) here. We also + // don't want to introduce another external dependency on + // golang.org/x/net/http2 at this time. For a complete recipe for how + // to enable HTTP/2 support on a transport suitable for the API client + // library see agent/http_test.go:TestHTTPServer_H2. + + if transport.TLSClientConfig == nil { + tlsClientConfig, err := api.SetupTLSConfig(&tlsConf) + + if err != nil { + return nil, err + } + + transport.TLSClientConfig = tlsClientConfig + } + + return client, nil +} + +// request is used to help build up a request +type request struct { + config *api.Config + method string + url *url.URL + params url.Values + body io.Reader + header http.Header + Obj interface{} + ctx context.Context +} + +// setQueryOptions is used to annotate the request with +// additional query options +func (r *request) SetQueryOptions(q *QueryOptions) { + if q == nil { + return + } + if q.Namespace != "" { + // For backwards-compatibility with existing tests, + // use the short-hand query param name "ns" + // rather than the alternative long-hand "namespace" + r.params.Set("ns", q.Namespace) + } + if q.Partition != "" { + // For backwards-compatibility with existing tests, + // use the long-hand query param name "partition" + // rather than the alternative short-hand "ap" + r.params.Set("partition", q.Partition) + } + if q.Datacenter != "" { + // For backwards-compatibility with existing tests, + // use the short-hand query param name "dc" + // rather than the alternative long-hand "datacenter" + r.params.Set("dc", q.Datacenter) + } + if q.Peer != "" { + r.params.Set("peer", q.Peer) + } + if q.AllowStale { + r.params.Set("stale", "") + } + if q.RequireConsistent { + r.params.Set("consistent", "") + } + if q.WaitIndex != 0 { + r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10)) + } + if q.WaitTime != 0 { + r.params.Set("wait", durToMsec(q.WaitTime)) + } + if q.WaitHash != "" { + r.params.Set("hash", q.WaitHash) + } + if q.Token != "" { + r.header.Set("X-Consul-Token", q.Token) + } + if q.Near != "" { + r.params.Set("near", q.Near) + } + if q.Filter != "" { + r.params.Set("filter", q.Filter) + } + if len(q.NodeMeta) > 0 { + for key, value := range q.NodeMeta { + r.params.Add("node-meta", key+":"+value) + } + } + if q.RelayFactor != 0 { + r.params.Set("relay-factor", strconv.Itoa(int(q.RelayFactor))) + } + if q.LocalOnly { + r.params.Set("local-only", fmt.Sprintf("%t", q.LocalOnly)) + } + if q.Connect { + r.params.Set("connect", "true") + } + if q.UseCache && !q.RequireConsistent { + r.params.Set("cached", "") + + cc := []string{} + if q.MaxAge > 0 { + cc = append(cc, fmt.Sprintf("max-age=%.0f", q.MaxAge.Seconds())) + } + if q.StaleIfError > 0 { + cc = append(cc, fmt.Sprintf("stale-if-error=%.0f", q.StaleIfError.Seconds())) + } + if len(cc) > 0 { + r.header.Set("Cache-Control", strings.Join(cc, ", ")) + } + } + if q.MergeCentralConfig { + r.params.Set("merge-central-config", "") + } + if q.Global { + r.params.Set("global", "") + } + + r.ctx = q.ctx +} + +// durToMsec converts a duration to a millisecond specified string. If the +// user selected a positive value that rounds to 0 ms, then we will use 1 ms +// so they get a short delay, otherwise Consul will translate the 0 ms into +// a huge default delay. +func durToMsec(dur time.Duration) string { + ms := dur / time.Millisecond + if dur > 0 && ms == 0 { + ms = 1 + } + return fmt.Sprintf("%dms", ms) +} + +// toHTTP converts the request to an HTTP request +func (r *request) toHTTP() (*http.Request, error) { + // Encode the query parameters + r.url.RawQuery = r.params.Encode() + + // Check if we should encode the body + if r.body == nil && r.Obj != nil { + b, err := encodeBody(r.Obj) + if err != nil { + return nil, err + } + r.body = b + } + + // Create the HTTP request + req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body) + if err != nil { + return nil, err + } + + // validate that socket communications that do not use the host, detect + // slashes in the host name and replace it with local host. + // this is required since go started validating req.host in 1.20.6 and 1.19.11. + // prior to that they would strip out the slashes for you. They removed that + // behavior and added more strict validation as part of a CVE. + // This issue is being tracked by the Go team: + // https://github.com/golang/go/issues/61431 + // If there is a resolution in this issue, we will remove this code. + // In the time being, this is the accepted workaround. + if strings.HasPrefix(r.url.Host, "/") { + r.url.Host = "localhost" + } + + req.URL.Host = r.url.Host + req.URL.Scheme = r.url.Scheme + req.Host = r.url.Host + req.Header = r.header + + // Content-Type must always be set when a body is present + // See https://github.com/hashicorp/consul/issues/10011 + if req.Body != nil && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "application/json") + } + + // Setup auth + if r.config.HttpAuth != nil { + req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password) + } + if r.ctx != nil { + return req.WithContext(r.ctx), nil + } + + return req, nil +} + +// newRequest is used to create a new request +func (c *Client) NewRequest(method, path string) *request { + r := &request{ + config: &c.config, + method: method, + url: &url.URL{ + Scheme: c.config.Scheme, + Host: c.config.Address, + Path: c.config.PathPrefix + path, + }, + params: make(map[string][]string), + header: c.Headers(), + } + + if c.config.Datacenter != "" { + r.params.Set("dc", c.config.Datacenter) + } + if c.config.Namespace != "" { + r.params.Set("ns", c.config.Namespace) + } + if c.config.Partition != "" { + r.params.Set("partition", c.config.Partition) + } + if c.config.WaitTime != 0 { + r.params.Set("wait", durToMsec(r.config.WaitTime)) + } + if c.config.Token != "" { + r.header.Set("X-Consul-Token", r.config.Token) + } + return r +} + +// doRequest runs a request with our client +func (c *Client) DoRequest(r *request) (time.Duration, *http.Response, error) { + req, err := r.toHTTP() + if err != nil { + return 0, nil, err + } + start := time.Now() + resp, err := c.config.HttpClient.Do(req) + diff := time.Since(start) + return diff, resp, err +} + +// DecodeBody is used to JSON decode a body +func DecodeBody(resp *http.Response, out interface{}) error { + dec := json.NewDecoder(resp.Body) + return dec.Decode(out) +} + +// encodeBody is used to encode a request body +func encodeBody(obj interface{}) (io.Reader, error) { + buf := bytes.NewBuffer(nil) + enc := json.NewEncoder(buf) + if err := enc.Encode(obj); err != nil { + return nil, err + } + return buf, nil +} + +// requireOK is used to wrap doRequest and check for a 200 +func RequireOK(resp *http.Response) error { + return RequireHttpCodes(resp, 200) +} + +// requireHttpCodes checks for the "allowable" http codes for a response +func RequireHttpCodes(resp *http.Response, httpCodes ...int) error { + // if there is an http code that we require, return w no error + for _, httpCode := range httpCodes { + if resp.StatusCode == httpCode { + return nil + } + } + + // if we reached here, then none of the http codes in resp matched any that we expected + // so err out + return generateUnexpectedResponseCodeError(resp) +} + +// closeResponseBody reads resp.Body until EOF, and then closes it. The read +// is necessary to ensure that the http.Client's underlying RoundTripper is able +// to re-use the TCP connection. See godoc on net/http.Client.Do. +func CloseResponseBody(resp *http.Response) error { + _, _ = io.Copy(io.Discard, resp.Body) + return resp.Body.Close() +} + +// generateUnexpectedResponseCodeError consumes the rest of the body, closes +// the body stream and generates an error indicating the status code was +// unexpected. +func generateUnexpectedResponseCodeError(resp *http.Response) error { + var buf bytes.Buffer + io.Copy(&buf, resp.Body) + CloseResponseBody(resp) + + trimmed := strings.TrimSpace(string(buf.Bytes())) + return StatusError{Code: resp.StatusCode, Body: trimmed} +} diff --git a/command/resource/delete/delete.go b/command/resource/delete/delete.go index 386ece85d7..2679951da8 100644 --- a/command/resource/delete/delete.go +++ b/command/resource/delete/delete.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/resource" + "github.com/hashicorp/consul/command/resource/client" ) func New(ui cli.Ui) *cmd { @@ -42,9 +43,9 @@ func (c *cmd) init() { } func (c *cmd) Run(args []string) int { - var gvk *api.GVK + var gvk *resource.GVK var resourceName string - var opts *api.QueryOptions + var opts *client.QueryOptions if err := c.flags.Parse(args); err != nil { if !errors.Is(err, flag.ErrHelp) { @@ -66,13 +67,13 @@ func (c *cmd) Run(args []string) int { return 1 } - gvk = &api.GVK{ + gvk = &resource.GVK{ Group: parsedResource.Id.Type.GetGroup(), Version: parsedResource.Id.Type.GetGroupVersion(), Kind: parsedResource.Id.Type.GetKind(), } resourceName = parsedResource.Id.GetName() - opts = &api.QueryOptions{ + opts = &client.QueryOptions{ Namespace: parsedResource.Id.Tenancy.GetNamespace(), Partition: parsedResource.Id.Tenancy.GetPartition(), Peer: parsedResource.Id.Tenancy.GetPeerName(), @@ -104,7 +105,7 @@ func (c *cmd) Run(args []string) int { c.UI.Error("Incorrect argument format: File argument is not needed when resource information is provided with the command") return 1 } - opts = &api.QueryOptions{ + opts = &client.QueryOptions{ Namespace: c.http.Namespace(), Partition: c.http.Partition(), Peer: c.http.PeerName(), @@ -112,13 +113,18 @@ func (c *cmd) Run(args []string) int { } } - client, err := c.http.APIClient() + config := api.DefaultConfig() + + c.http.MergeOntoConfig(config) + resourceClient, err := client.NewClient(config) if err != nil { c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) return 1 } - if err := client.Resource().Delete(gvk, resourceName, opts); err != nil { + res := resource.Resource{C: resourceClient} + + if err := res.Delete(gvk, resourceName, opts); err != nil { c.UI.Error(fmt.Sprintf("Error deleting resource %s.%s.%s/%s: %v", gvk.Group, gvk.Version, gvk.Kind, resourceName, err)) return 1 } diff --git a/command/resource/helper.go b/command/resource/helper.go index 6299237d2e..aa8d45e02b 100644 --- a/command/resource/helper.go +++ b/command/resource/helper.go @@ -7,11 +7,12 @@ import ( "errors" "flag" "fmt" + "net/http" "strings" "github.com/hashicorp/consul/agent/consul" - "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/helpers" + "github.com/hashicorp/consul/command/resource/client" "github.com/hashicorp/consul/internal/resourcehcl" "github.com/hashicorp/consul/proto-public/pbresource" ) @@ -38,7 +39,7 @@ func ParseInputParams(inputArgs []string, flags *flag.FlagSet) error { return nil } -func GetTypeAndResourceName(args []string) (gvk *api.GVK, resourceName string, e error) { +func GetTypeAndResourceName(args []string) (gvk *GVK, resourceName string, e error) { // 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") @@ -49,7 +50,7 @@ func GetTypeAndResourceName(args []string) (gvk *api.GVK, resourceName string, e return nil, "", fmt.Errorf("Must include resource type argument in group.verion.kind format") } - gvk = &api.GVK{ + gvk = &GVK{ Group: s[0], Version: s[1], Kind: s[2], @@ -58,3 +59,101 @@ func GetTypeAndResourceName(args []string) (gvk *api.GVK, resourceName string, e resourceName = args[1] return } + +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"` +} + +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 +} diff --git a/command/resource/list/list.go b/command/resource/list/list.go index fb097e90d0..d0a5f398a6 100644 --- a/command/resource/list/list.go +++ b/command/resource/list/list.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/resource" + "github.com/hashicorp/consul/command/resource/client" ) func New(ui cli.Ui) *cmd { @@ -44,8 +45,8 @@ func (c *cmd) init() { } func (c *cmd) Run(args []string) int { - var gvk *api.GVK - var opts *api.QueryOptions + var gvk *resource.GVK + var opts *client.QueryOptions if err := c.flags.Parse(args); err != nil { if !errors.Is(err, flag.ErrHelp) { @@ -67,12 +68,12 @@ func (c *cmd) Run(args []string) int { return 1 } - gvk = &api.GVK{ + gvk = &resource.GVK{ Group: parsedResource.Id.Type.GetGroup(), Version: parsedResource.Id.Type.GetGroupVersion(), Kind: parsedResource.Id.Type.GetKind(), } - opts = &api.QueryOptions{ + opts = &client.QueryOptions{ Namespace: parsedResource.Id.Tenancy.GetNamespace(), Partition: parsedResource.Id.Tenancy.GetPartition(), Peer: parsedResource.Id.Tenancy.GetPeerName(), @@ -103,7 +104,7 @@ func (c *cmd) Run(args []string) int { return 1 } - opts = &api.QueryOptions{ + opts = &client.QueryOptions{ Namespace: c.http.Namespace(), Partition: c.http.Partition(), Peer: c.http.PeerName(), @@ -112,13 +113,18 @@ func (c *cmd) Run(args []string) int { } } - client, err := c.http.APIClient() + config := api.DefaultConfig() + + c.http.MergeOntoConfig(config) + resourceClient, err := client.NewClient(config) if err != nil { c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) return 1 } - entry, err := client.Resource().List(gvk, opts) + res := resource.Resource{C: resourceClient} + + entry, err := res.List(gvk, opts) if err != nil { c.UI.Error(fmt.Sprintf("Error reading resources for type %s: %v", gvk, err)) return 1 @@ -134,7 +140,7 @@ func (c *cmd) Run(args []string) int { return 0 } -func getResourceType(args []string) (gvk *api.GVK, e error) { +func getResourceType(args []string) (gvk *resource.GVK, e error) { if len(args) < 1 { return nil, fmt.Errorf("Must include resource type argument") } @@ -147,7 +153,7 @@ func getResourceType(args []string) (gvk *api.GVK, e error) { if len(s) < 3 { return nil, fmt.Errorf("Must include resource type argument in group.verion.kind format") } - gvk = &api.GVK{ + gvk = &resource.GVK{ Group: s[0], Version: s[1], Kind: s[2], diff --git a/command/resource/read/read.go b/command/resource/read/read.go index 48d93eccaa..fe35521056 100644 --- a/command/resource/read/read.go +++ b/command/resource/read/read.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/resource" + "github.com/hashicorp/consul/command/resource/client" ) func New(ui cli.Ui) *cmd { @@ -43,9 +44,9 @@ func (c *cmd) init() { } func (c *cmd) Run(args []string) int { - var gvk *api.GVK + var gvk *resource.GVK var resourceName string - var opts *api.QueryOptions + var opts *client.QueryOptions if err := c.flags.Parse(args); err != nil { if !errors.Is(err, flag.ErrHelp) { @@ -67,13 +68,13 @@ func (c *cmd) Run(args []string) int { return 1 } - gvk = &api.GVK{ + gvk = &resource.GVK{ Group: parsedResource.Id.Type.GetGroup(), Version: parsedResource.Id.Type.GetGroupVersion(), Kind: parsedResource.Id.Type.GetKind(), } resourceName = parsedResource.Id.GetName() - opts = &api.QueryOptions{ + opts = &client.QueryOptions{ Namespace: parsedResource.Id.Tenancy.GetNamespace(), Partition: parsedResource.Id.Tenancy.GetPartition(), Peer: parsedResource.Id.Tenancy.GetPeerName(), @@ -106,7 +107,7 @@ func (c *cmd) Run(args []string) int { c.UI.Error("Incorrect argument format: File argument is not needed when resource information is provided with the command") return 1 } - opts = &api.QueryOptions{ + opts = &client.QueryOptions{ Namespace: c.http.Namespace(), Partition: c.http.Partition(), Peer: c.http.PeerName(), @@ -115,13 +116,18 @@ func (c *cmd) Run(args []string) int { } } - client, err := c.http.APIClient() + config := api.DefaultConfig() + + c.http.MergeOntoConfig(config) + resourceClient, err := client.NewClient(config) if err != nil { c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) return 1 } - entry, err := client.Resource().Read(gvk, resourceName, opts) + res := resource.Resource{C: resourceClient} + + entry, err := res.Read(gvk, resourceName, opts) if err != nil { c.UI.Error(fmt.Sprintf("Error reading resource %s/%s: %v", gvk, resourceName, err)) return 1 diff --git a/envoyextensions/go.mod b/envoyextensions/go.mod index 495c908b8b..4d4301841e 100644 --- a/envoyextensions/go.mod +++ b/envoyextensions/go.mod @@ -4,7 +4,6 @@ go 1.20 replace ( github.com/hashicorp/consul/api => ../api - github.com/hashicorp/consul/proto-public => ../proto-public github.com/hashicorp/consul/sdk => ../sdk ) @@ -28,7 +27,6 @@ require ( github.com/envoyproxy/protoc-gen-validate v0.10.0 // indirect github.com/fatih/color v1.14.1 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/hashicorp/consul/proto-public v0.4.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect @@ -43,10 +41,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect - golang.org/x/net v0.13.0 // indirect golang.org/x/sys v0.11.0 // indirect - golang.org/x/text v0.11.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.55.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/envoyextensions/go.sum b/envoyextensions/go.sum index b2117f230c..de21ca6c0b 100644 --- a/envoyextensions/go.sum +++ b/envoyextensions/go.sum @@ -201,7 +201,6 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= -golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -237,8 +236,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -256,8 +253,6 @@ google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaL google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= diff --git a/go.mod b/go.mod index d828a475a5..b89b5d420c 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-raftchunking v0.7.0 github.com/hashicorp/go-retryablehttp v0.6.7 + github.com/hashicorp/go-rootcerts v1.0.2 github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 github.com/hashicorp/go-sockaddr v1.0.2 github.com/hashicorp/go-syslog v1.0.0 @@ -198,7 +199,6 @@ require ( github.com/hashicorp/go-msgpack v0.5.5 // indirect github.com/hashicorp/go-msgpack/v2 v2.0.0 // indirect github.com/hashicorp/go-plugin v1.4.5 // indirect - github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect diff --git a/troubleshoot/go.mod b/troubleshoot/go.mod index 6a694ace24..a12fbde8a1 100644 --- a/troubleshoot/go.mod +++ b/troubleshoot/go.mod @@ -30,7 +30,6 @@ require ( github.com/envoyproxy/protoc-gen-validate v0.10.0 // indirect github.com/fatih/color v1.14.1 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/hashicorp/consul/proto-public v0.4.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect @@ -40,12 +39,14 @@ require ( github.com/hashicorp/go-version v1.2.1 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/serf v0.10.1 // indirect + github.com/kr/pretty v0.3.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.3.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect golang.org/x/net v0.13.0 // indirect @@ -53,5 +54,6 @@ require ( golang.org/x/text v0.11.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.55.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/troubleshoot/go.sum b/troubleshoot/go.sum index d2388efd13..b5f44a5e8f 100644 --- a/troubleshoot/go.sum +++ b/troubleshoot/go.sum @@ -70,6 +70,7 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195 h1:58f1tJ1ra+zFINPlwLWvQsR9CzAKt2e+EWV2yX9oXQ4= github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -210,10 +211,13 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -271,7 +275,9 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -585,6 +591,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=