8a1e53754e
This config entry will be used to configure terminating gateways. It accepts the name of the gateway and a list of services the gateway will represent. For each service users will be able to specify: its name, namespace, and additional options for TLS origination. Co-authored-by: Kyle Havlovitz <kylehav@gmail.com> Co-authored-by: Chris Piraino <cpiraino@hashicorp.com>
354 lines
9.5 KiB
Go
354 lines
9.5 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
const (
|
|
ServiceDefaults string = "service-defaults"
|
|
ProxyDefaults string = "proxy-defaults"
|
|
ServiceRouter string = "service-router"
|
|
ServiceSplitter string = "service-splitter"
|
|
ServiceResolver string = "service-resolver"
|
|
IngressGateway string = "ingress-gateway"
|
|
TerminatingGateway string = "terminating-gateway"
|
|
|
|
ProxyConfigGlobal string = "global"
|
|
)
|
|
|
|
type ConfigEntry interface {
|
|
GetKind() string
|
|
GetName() string
|
|
GetCreateIndex() uint64
|
|
GetModifyIndex() uint64
|
|
}
|
|
|
|
type MeshGatewayMode string
|
|
|
|
const (
|
|
// MeshGatewayModeDefault represents no specific mode and should
|
|
// be used to indicate that a different layer of the configuration
|
|
// chain should take precedence
|
|
MeshGatewayModeDefault MeshGatewayMode = ""
|
|
|
|
// MeshGatewayModeNone represents that the Upstream Connect connections
|
|
// should be direct and not flow through a mesh gateway.
|
|
MeshGatewayModeNone MeshGatewayMode = "none"
|
|
|
|
// MeshGatewayModeLocal represents that the Upstrea Connect connections
|
|
// should be made to a mesh gateway in the local datacenter. This is
|
|
MeshGatewayModeLocal MeshGatewayMode = "local"
|
|
|
|
// MeshGatewayModeRemote represents that the Upstream Connect connections
|
|
// should be made to a mesh gateway in a remote datacenter.
|
|
MeshGatewayModeRemote MeshGatewayMode = "remote"
|
|
)
|
|
|
|
// MeshGatewayConfig controls how Mesh Gateways are used for upstream Connect
|
|
// services
|
|
type MeshGatewayConfig struct {
|
|
// Mode is the mode that should be used for the upstream connection.
|
|
Mode MeshGatewayMode `json:",omitempty"`
|
|
}
|
|
|
|
// ExposeConfig describes HTTP paths to expose through Envoy outside of Connect.
|
|
// Users can expose individual paths and/or all HTTP/GRPC paths for checks.
|
|
type ExposeConfig struct {
|
|
// Checks defines whether paths associated with Consul checks will be exposed.
|
|
// This flag triggers exposing all HTTP and GRPC check paths registered for the service.
|
|
Checks bool `json:",omitempty"`
|
|
|
|
// Paths is the list of paths exposed through the proxy.
|
|
Paths []ExposePath `json:",omitempty"`
|
|
}
|
|
|
|
type ExposePath struct {
|
|
// ListenerPort defines the port of the proxy's listener for exposed paths.
|
|
ListenerPort int `json:",omitempty"`
|
|
|
|
// Path is the path to expose through the proxy, ie. "/metrics."
|
|
Path string `json:",omitempty"`
|
|
|
|
// LocalPathPort is the port that the service is listening on for the given path.
|
|
LocalPathPort int `json:",omitempty"`
|
|
|
|
// Protocol describes the upstream's service protocol.
|
|
// Valid values are "http" and "http2", defaults to "http"
|
|
Protocol string `json:",omitempty"`
|
|
|
|
// ParsedFromCheck is set if this path was parsed from a registered check
|
|
ParsedFromCheck bool
|
|
}
|
|
|
|
type ServiceConfigEntry struct {
|
|
Kind string
|
|
Name string
|
|
Namespace string `json:",omitempty"`
|
|
Protocol string `json:",omitempty"`
|
|
MeshGateway MeshGatewayConfig `json:",omitempty"`
|
|
Expose ExposeConfig `json:",omitempty"`
|
|
ExternalSNI string `json:",omitempty"`
|
|
CreateIndex uint64
|
|
ModifyIndex uint64
|
|
}
|
|
|
|
func (s *ServiceConfigEntry) GetKind() string {
|
|
return s.Kind
|
|
}
|
|
|
|
func (s *ServiceConfigEntry) GetName() string {
|
|
return s.Name
|
|
}
|
|
|
|
func (s *ServiceConfigEntry) GetCreateIndex() uint64 {
|
|
return s.CreateIndex
|
|
}
|
|
|
|
func (s *ServiceConfigEntry) GetModifyIndex() uint64 {
|
|
return s.ModifyIndex
|
|
}
|
|
|
|
type ProxyConfigEntry struct {
|
|
Kind string
|
|
Name string
|
|
Namespace string `json:",omitempty"`
|
|
Config map[string]interface{} `json:",omitempty"`
|
|
MeshGateway MeshGatewayConfig `json:",omitempty"`
|
|
Expose ExposeConfig `json:",omitempty"`
|
|
CreateIndex uint64
|
|
ModifyIndex uint64
|
|
}
|
|
|
|
func (p *ProxyConfigEntry) GetKind() string {
|
|
return p.Kind
|
|
}
|
|
|
|
func (p *ProxyConfigEntry) GetName() string {
|
|
return p.Name
|
|
}
|
|
|
|
func (p *ProxyConfigEntry) GetCreateIndex() uint64 {
|
|
return p.CreateIndex
|
|
}
|
|
|
|
func (p *ProxyConfigEntry) GetModifyIndex() uint64 {
|
|
return p.ModifyIndex
|
|
}
|
|
|
|
func makeConfigEntry(kind, name string) (ConfigEntry, error) {
|
|
switch kind {
|
|
case ServiceDefaults:
|
|
return &ServiceConfigEntry{Kind: kind, Name: name}, nil
|
|
case ProxyDefaults:
|
|
return &ProxyConfigEntry{Kind: kind, Name: name}, nil
|
|
case ServiceRouter:
|
|
return &ServiceRouterConfigEntry{Kind: kind, Name: name}, nil
|
|
case ServiceSplitter:
|
|
return &ServiceSplitterConfigEntry{Kind: kind, Name: name}, nil
|
|
case ServiceResolver:
|
|
return &ServiceResolverConfigEntry{Kind: kind, Name: name}, nil
|
|
case IngressGateway:
|
|
return &IngressGatewayConfigEntry{Kind: kind, Name: name}, nil
|
|
case TerminatingGateway:
|
|
return &TerminatingGatewayConfigEntry{Kind: kind, Name: name}, nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid config entry kind: %s", kind)
|
|
}
|
|
}
|
|
|
|
func MakeConfigEntry(kind, name string) (ConfigEntry, error) {
|
|
return makeConfigEntry(kind, name)
|
|
}
|
|
|
|
// DecodeConfigEntry will decode the result of using json.Unmarshal of a config
|
|
// entry into a map[string]interface{}.
|
|
//
|
|
// Important caveats:
|
|
//
|
|
// - This will NOT work if the map[string]interface{} was produced using HCL
|
|
// decoding as that requires more extensive parsing to work around the issues
|
|
// with map[string][]interface{} that arise.
|
|
//
|
|
// - This will only decode fields using their camel case json field
|
|
// representations.
|
|
func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
|
|
var entry ConfigEntry
|
|
|
|
kindVal, ok := raw["Kind"]
|
|
if !ok {
|
|
kindVal, ok = raw["kind"]
|
|
}
|
|
if !ok {
|
|
return nil, fmt.Errorf("Payload does not contain a kind/Kind key at the top level")
|
|
}
|
|
|
|
if kindStr, ok := kindVal.(string); ok {
|
|
newEntry, err := makeConfigEntry(kindStr, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entry = newEntry
|
|
} else {
|
|
return nil, fmt.Errorf("Kind value in payload is not a string")
|
|
}
|
|
|
|
decodeConf := &mapstructure.DecoderConfig{
|
|
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
|
Result: &entry,
|
|
WeaklyTypedInput: true,
|
|
}
|
|
|
|
decoder, err := mapstructure.NewDecoder(decodeConf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return entry, decoder.Decode(raw)
|
|
}
|
|
|
|
func DecodeConfigEntryFromJSON(data []byte) (ConfigEntry, error) {
|
|
var raw map[string]interface{}
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return DecodeConfigEntry(raw)
|
|
}
|
|
|
|
func decodeConfigEntrySlice(raw []map[string]interface{}) ([]ConfigEntry, error) {
|
|
var entries []ConfigEntry
|
|
for _, rawEntry := range raw {
|
|
entry, err := DecodeConfigEntry(rawEntry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
// ConfigEntries can be used to query the Config endpoints
|
|
type ConfigEntries struct {
|
|
c *Client
|
|
}
|
|
|
|
// Config returns a handle to the Config endpoints
|
|
func (c *Client) ConfigEntries() *ConfigEntries {
|
|
return &ConfigEntries{c}
|
|
}
|
|
|
|
func (conf *ConfigEntries) Get(kind string, name string, q *QueryOptions) (ConfigEntry, *QueryMeta, error) {
|
|
if kind == "" || name == "" {
|
|
return nil, nil, fmt.Errorf("Both kind and name parameters must not be empty")
|
|
}
|
|
|
|
entry, err := makeConfigEntry(kind, name)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s/%s", kind, name))
|
|
r.setQueryOptions(q)
|
|
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
qm := &QueryMeta{}
|
|
parseQueryMeta(resp, qm)
|
|
qm.RequestTime = rtt
|
|
|
|
if err := decodeBody(resp, entry); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return entry, qm, nil
|
|
}
|
|
|
|
func (conf *ConfigEntries) List(kind string, q *QueryOptions) ([]ConfigEntry, *QueryMeta, error) {
|
|
if kind == "" {
|
|
return nil, nil, fmt.Errorf("The kind parameter must not be empty")
|
|
}
|
|
|
|
r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s", kind))
|
|
r.setQueryOptions(q)
|
|
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
qm := &QueryMeta{}
|
|
parseQueryMeta(resp, qm)
|
|
qm.RequestTime = rtt
|
|
|
|
var raw []map[string]interface{}
|
|
if err := decodeBody(resp, &raw); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
entries, err := decodeConfigEntrySlice(raw)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return entries, qm, nil
|
|
}
|
|
|
|
func (conf *ConfigEntries) Set(entry ConfigEntry, w *WriteOptions) (bool, *WriteMeta, error) {
|
|
return conf.set(entry, nil, w)
|
|
}
|
|
|
|
func (conf *ConfigEntries) CAS(entry ConfigEntry, index uint64, w *WriteOptions) (bool, *WriteMeta, error) {
|
|
return conf.set(entry, map[string]string{"cas": strconv.FormatUint(index, 10)}, w)
|
|
}
|
|
|
|
func (conf *ConfigEntries) set(entry ConfigEntry, params map[string]string, w *WriteOptions) (bool, *WriteMeta, error) {
|
|
r := conf.c.newRequest("PUT", "/v1/config")
|
|
r.setWriteOptions(w)
|
|
for param, value := range params {
|
|
r.params.Set(param, value)
|
|
}
|
|
r.obj = entry
|
|
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var buf bytes.Buffer
|
|
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
|
return false, nil, fmt.Errorf("Failed to read response: %v", err)
|
|
}
|
|
res := strings.Contains(buf.String(), "true")
|
|
|
|
wm := &WriteMeta{RequestTime: rtt}
|
|
return res, wm, nil
|
|
}
|
|
|
|
func (conf *ConfigEntries) Delete(kind string, name string, w *WriteOptions) (*WriteMeta, error) {
|
|
if kind == "" || name == "" {
|
|
return nil, fmt.Errorf("Both kind and name parameters must not be empty")
|
|
}
|
|
|
|
r := conf.c.newRequest("DELETE", fmt.Sprintf("/v1/config/%s/%s", kind, name))
|
|
r.setWriteOptions(w)
|
|
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.Body.Close()
|
|
wm := &WriteMeta{RequestTime: rtt}
|
|
return wm, nil
|
|
}
|