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