github.com/sl1pm4t/consul@v1.4.5-0.20190325224627-74c31c540f9c/agent/xds/clusters.go (about)

     1  package xds
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"strconv"
     8  	"time"
     9  
    10  	envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
    11  	envoyauth "github.com/envoyproxy/go-control-plane/envoy/api/v2/auth"
    12  	envoycluster "github.com/envoyproxy/go-control-plane/envoy/api/v2/cluster"
    13  	envoycore "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
    14  	"github.com/gogo/protobuf/jsonpb"
    15  	"github.com/gogo/protobuf/proto"
    16  	"github.com/gogo/protobuf/types"
    17  
    18  	"github.com/hashicorp/consul/agent/proxycfg"
    19  	"github.com/hashicorp/consul/agent/structs"
    20  )
    21  
    22  // clustersFromSnapshot returns the xDS API representation of the "clusters"
    23  // (upstreams) in the snapshot.
    24  func clustersFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot, token string) ([]proto.Message, error) {
    25  	if cfgSnap == nil {
    26  		return nil, errors.New("nil config given")
    27  	}
    28  	// Include the "app" cluster for the public listener
    29  	clusters := make([]proto.Message, len(cfgSnap.Proxy.Upstreams)+1)
    30  
    31  	var err error
    32  	clusters[0], err = makeAppCluster(cfgSnap)
    33  	if err != nil {
    34  		return nil, err
    35  	}
    36  
    37  	for idx, upstream := range cfgSnap.Proxy.Upstreams {
    38  		clusters[idx+1], err = makeUpstreamCluster(upstream, cfgSnap)
    39  		if err != nil {
    40  			return nil, err
    41  		}
    42  	}
    43  
    44  	return clusters, nil
    45  }
    46  
    47  func makeAppCluster(cfgSnap *proxycfg.ConfigSnapshot) (*envoy.Cluster, error) {
    48  	var c *envoy.Cluster
    49  	var err error
    50  
    51  	// If we have overridden local cluster config try to parse it into an Envoy cluster
    52  	if clusterJSONRaw, ok := cfgSnap.Proxy.Config["envoy_local_cluster_json"]; ok {
    53  		if clusterJSON, ok := clusterJSONRaw.(string); ok {
    54  			c, err = makeClusterFromUserConfig(clusterJSON)
    55  			if err != nil {
    56  				return c, err
    57  			}
    58  		}
    59  	}
    60  
    61  	if c == nil {
    62  		addr := cfgSnap.Proxy.LocalServiceAddress
    63  		if addr == "" {
    64  			addr = "127.0.0.1"
    65  		}
    66  		c = &envoy.Cluster{
    67  			Name:           LocalAppClusterName,
    68  			ConnectTimeout: 5 * time.Second,
    69  			Type:           envoy.Cluster_STATIC,
    70  			// API v2 docs say hosts is deprecated and should use LoadAssignment as
    71  			// below.. but it doesn't work for tcp_proxy target for some reason.
    72  			Hosts: []*envoycore.Address{makeAddressPtr(addr, cfgSnap.Proxy.LocalServicePort)},
    73  			// LoadAssignment: &envoy.ClusterLoadAssignment{
    74  			//  ClusterName: LocalAppClusterName,
    75  			//  Endpoints: []endpoint.LocalityLbEndpoints{
    76  			//    {
    77  			//      LbEndpoints: []endpoint.LbEndpoint{
    78  			//        makeEndpoint(LocalAppClusterName,
    79  			//          addr,
    80  			//          cfgSnap.Proxy.LocalServicePort),
    81  			//      },
    82  			//    },
    83  			//  },
    84  			// },
    85  		}
    86  	}
    87  
    88  	return c, err
    89  }
    90  
    91  func parseTimeMillis(ms interface{}) (time.Duration, error) {
    92  	switch v := ms.(type) {
    93  	case string:
    94  		ms, err := strconv.Atoi(v)
    95  		if err != nil {
    96  			return 0, err
    97  		}
    98  		return time.Duration(ms) * time.Millisecond, nil
    99  
   100  	case float64: // This is what parsing from JSON results in
   101  		return time.Duration(v) * time.Millisecond, nil
   102  	// Not sure if this can ever really happen but just in case it does in
   103  	// some test code...
   104  	case int:
   105  		return time.Duration(v) * time.Millisecond, nil
   106  	}
   107  	return 0, errors.New("invalid type for millisecond duration")
   108  }
   109  
   110  func makeUpstreamCluster(upstream structs.Upstream, cfgSnap *proxycfg.ConfigSnapshot) (*envoy.Cluster, error) {
   111  	var c *envoy.Cluster
   112  	var err error
   113  
   114  	// If we have overridden cluster config attempt to parse it into an Envoy cluster
   115  	if clusterJSONRaw, ok := upstream.Config["envoy_cluster_json"]; ok {
   116  		if clusterJSON, ok := clusterJSONRaw.(string); ok {
   117  			c, err = makeClusterFromUserConfig(clusterJSON)
   118  			if err != nil {
   119  				return c, err
   120  			}
   121  		}
   122  	}
   123  
   124  	if c == nil {
   125  		conTimeout := 5 * time.Second
   126  		if toRaw, ok := upstream.Config["connect_timeout_ms"]; ok {
   127  			if ms, err := parseTimeMillis(toRaw); err == nil {
   128  				conTimeout = ms
   129  			}
   130  		}
   131  		c = &envoy.Cluster{
   132  			Name:           upstream.Identifier(),
   133  			ConnectTimeout: conTimeout,
   134  			Type:           envoy.Cluster_EDS,
   135  			EdsClusterConfig: &envoy.Cluster_EdsClusterConfig{
   136  				EdsConfig: &envoycore.ConfigSource{
   137  					ConfigSourceSpecifier: &envoycore.ConfigSource_Ads{
   138  						Ads: &envoycore.AggregatedConfigSource{},
   139  					},
   140  				},
   141  			},
   142  			// Having an empty config enables outlier detection with default config.
   143  			OutlierDetection: &envoycluster.OutlierDetection{},
   144  		}
   145  	}
   146  
   147  	// Enable TLS upstream with the configured client certificate.
   148  	c.TlsContext = &envoyauth.UpstreamTlsContext{
   149  		CommonTlsContext: makeCommonTLSContext(cfgSnap),
   150  	}
   151  
   152  	return c, nil
   153  }
   154  
   155  // makeClusterFromUserConfig returns the listener config decoded from an
   156  // arbitrary proto3 json format string or an error if it's invalid.
   157  //
   158  // For now we only support embedding in JSON strings because of the hcl parsing
   159  // pain (see config.go comment above call to patchSliceOfMaps). Until we
   160  // refactor config parser a _lot_ user's opaque config that contains arrays will
   161  // be mangled. We could actually fix that up in mapstructure which knows the
   162  // type of the target so could resolve the slices to singletons unambiguously
   163  // and it would work for us here... but we still have the problem that the
   164  // config would render incorrectly in general in our HTTP API responses so we
   165  // really need to fix it "properly".
   166  //
   167  // When we do that we can support just nesting the config directly into the
   168  // JSON/hcl naturally but this is a stop-gap that gets us an escape hatch
   169  // immediately. It's also probably not a bad thing to support long-term since
   170  // any config generated by other systems will likely be in canonical protobuf
   171  // from rather than our slight variant in JSON/hcl.
   172  func makeClusterFromUserConfig(configJSON string) (*envoy.Cluster, error) {
   173  	var jsonFields map[string]*json.RawMessage
   174  	if err := json.Unmarshal([]byte(configJSON), &jsonFields); err != nil {
   175  		fmt.Println("Custom error", err, configJSON)
   176  		return nil, err
   177  	}
   178  
   179  	var c envoy.Cluster
   180  
   181  	if _, ok := jsonFields["@type"]; ok {
   182  		// Type field is present so decode it as a types.Any
   183  		var any types.Any
   184  		err := jsonpb.UnmarshalString(configJSON, &any)
   185  		if err != nil {
   186  			return nil, err
   187  		}
   188  		// And then unmarshal the listener again...
   189  		err = proto.Unmarshal(any.Value, &c)
   190  		if err != nil {
   191  			panic(err)
   192  			//return nil, err
   193  		}
   194  		return &c, err
   195  	}
   196  
   197  	// No @type so try decoding as a straight listener.
   198  	err := jsonpb.UnmarshalString(configJSON, &c)
   199  	return &c, err
   200  }