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 }