├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bootstrap.go ├── configstore.go ├── controller.go ├── envoy.go ├── epstore.go ├── example ├── README.md ├── envoy │ ├── Dockerfile │ └── envoy.yaml ├── k8s │ ├── configmap.yaml │ ├── envoy.yaml │ ├── foobar.yaml │ └── xds.yaml ├── runtests.sh └── tests │ ├── envoy.bats │ └── xds.bats ├── go.mod ├── go.sum ├── http.go ├── http_test.go ├── main.go ├── proxy.go └── utils.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | *.yaml 3 | Makefile 4 | xds 5 | example/ 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | xds 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | script: 4 | - env GO111MODULE=on go mod download 5 | - env GO111MODULE=on go test -v 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.15.8-alpine3.13 AS builder 2 | 3 | RUN mkdir -p /usr/src/app 4 | WORKDIR /usr/src/app 5 | 6 | RUN apk add --no-cache git 7 | 8 | COPY go.mod go.sum ./ 9 | RUN go mod download 10 | 11 | COPY . ./ 12 | 13 | RUN go build -v -o /bin/xds 14 | 15 | FROM envoyproxy/envoy-alpine:v1.16.0 16 | 17 | COPY --from=builder /bin/xds /bin/xds 18 | 19 | ENTRYPOINT ["/bin/xds"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | eds.yaml: eds.template.yaml 2 | jinja2 eds.template.yaml > eds.yaml 3 | 4 | docker: clean 5 | docker build --pull --rm -t us.gcr.io/sentryio/xds:latest . 6 | 7 | push: docker 8 | docker push us.gcr.io/sentryio/xds:latest 9 | 10 | deploy: push 11 | kubectl -n sentry-system scale deployment xds --replicas=0 12 | kubectl -n sentry-system scale deployment xds --replicas=1 13 | 14 | clean: 15 | rm -f ./xds 16 | 17 | test: 18 | go test -v 19 | 20 | .PHONY: docker push deploy clean test 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xDS 2 | 3 | Implementation of [Envoy's](https://www.envoyproxy.io/) dynamic resources discovery [xDS REST](https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol). 4 | 5 | xDS is fundamentally an HTTP service that is hit by every Envoy process to get its state of listeners (LDS), clusters (CDS) and subsequently each cluster's endpoints through (EDS). 6 | 7 | It's tightly coupled to Kubernetes: 8 | - Uses config map for configuration. 9 | - Cluster endpoints are Kubernetes service endpoints. 10 | 11 | Limitations: 12 | - Supports only services exposing one port. Services exposing multiple ports will are ignored. 13 | 14 | 15 | ## Configuration 16 | 17 | xDS uses environment variables for configuration: 18 | 19 | - **XDS_CONFIGMAP** - Path to the configuration configmap in form `{namespace}/{configmap.name}`. Defaults to `default/xds`. 20 | - **XDS_LISTEN** - Socket address for the http server. Defaults to `127.0.0.1:5000`. 21 | 22 | 23 | ## Running 24 | 25 | Build a docker image: 26 | 27 | ``` 28 | go build xds 29 | ``` 30 | 31 | For xds to run, you need access to Kubernetes cluster. During startup xDS will try to infer by reading the `~/.kube/config` file with fallback to a in cluster config. 32 | 33 | Assume you have local cluster running, accessible and the configuration loaded: 34 | 35 | ``` 36 | ./xds 37 | 2020/05/22 15:24:52 configstore.go:146: ConfigStore.Init: default xds default/xds 38 | 2020/05/22 15:24:52 main.go:106: ready. 39 | ``` 40 | 41 | For testing out use the example configmap at `example/k8s/configmap.yaml`. 42 | 43 | 44 | ## Configuration validation 45 | 46 | Validate configmap using `--validate` cli argument: 47 | 48 | ``` 49 | ./xds --validate path/to/configmap.yaml 50 | 51 | # or from stdin 52 | 53 | render_my_configmap | ./xds --validate - 54 | ``` 55 | 56 | Or by `POST`ing to the `/validate` endpoint: 57 | 58 | ``` 59 | curl localhost:5000/validate --data-binary @example/k8s/configmap.yaml 60 | ok 61 | 62 | # or 63 | 64 | render_my_configmap | curl localhost:5000/validate --data-binary @- 65 | ok 66 | ``` 67 | 68 | 69 | ## Inspecting 70 | 71 | These can easily be introspected through the HTTP API with `curl`. 72 | 73 | LDS - http://xds.service.sentry.internal/v2/discovery:listeners
74 | CDS - http://xds.service.sentry.internal/v2/discovery:clusters
75 | EDS - http://xds.service.sentry.internal/v2/discovery:endpoints 76 | 77 | 78 | Both LDS and CDS only need information about the host it's querying about, whereas EDS needs to know what service it's asking about. 79 | 80 | An example LDS request looks like: 81 | 82 | ```shell 83 | % curl -s -XPOST -d '{"node": {"id": "xxx", "cluster":"snuba"}}' xds.service.sentry.internal/v2/discovery:listeners | jq . 84 | { 85 | "version_info": "0", 86 | "resources": [ 87 | { 88 | "@type": "type.googleapis.com/envoy.api.v2.Listener", 89 | "name": "snuba-query-tcp", 90 | "address": { 91 | "socket_address": { 92 | "address": "127.0.0.1", 93 | "port_value": 9000 94 | } 95 | }, 96 | "filter_chains": [ 97 | { 98 | "filters": [ 99 | { 100 | "name": "envoy.tcp_proxy", 101 | "typed_config": { 102 | "@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy", 103 | "stat_prefix": "snuba-query-tcp", 104 | "cluster": "snuba-query-tcp" 105 | } 106 | } 107 | ] 108 | } 109 | ] 110 | } 111 | ] 112 | } 113 | ``` 114 | 115 | The request is sending along a node id, and a node cluster assignment. This relates to the `assignments` dataset in our `ConfigMap` if we want to make sure that the correct listeners are being served for `snuba`. 116 | 117 | This exact query can be made against the CDS endpoint to get the cluster assignments. 118 | 119 | From there, the only weird one is EDS, which is probably the most important one. Since EDS returns back the list of endpoints for a specific backend. 120 | 121 | Example: 122 | 123 | ```shell 124 | % curl -s -XPOST -d '{"node": {"id": "xxx", "cluster":"snuba"},"resource_names":["default/snuba-query-tcp"]}' xds.service.sentry.internal/v2/discovery:endpoints | jq . 125 | { 126 | "version_info": "0", 127 | "resources": [ 128 | { 129 | "@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment", 130 | "cluster_name": "default/snuba-query-tcp", 131 | "endpoints": [ 132 | { 133 | "lb_endpoints": [ 134 | { 135 | "endpoint": { 136 | "address": { 137 | "socket_address": { 138 | "address": "192.168.208.109", 139 | "port_value": 9000 140 | } 141 | } 142 | } 143 | }, 144 | { 145 | "endpoint": { 146 | "address": { 147 | "socket_address": { 148 | "address": "192.168.208.139", 149 | "port_value": 9000 150 | } 151 | } 152 | } 153 | } 154 | ] 155 | } 156 | ] 157 | } 158 | ] 159 | } 160 | ``` 161 | 162 | For EDS, it takes an extra "resource_names" key to match the cluster_name inside of the cluster definition. 163 | -------------------------------------------------------------------------------- /bootstrap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "sync" 8 | ) 9 | 10 | type bootstrapData struct { 11 | Clusters []byte `json:"clusters"` 12 | Listeners []byte `json:"listeners"` 13 | Endpoints map[string][]byte `json:"endpoints"` 14 | endpointsLock sync.Mutex 15 | } 16 | 17 | func readBootstrapData(path string) (*bootstrapData, error) { 18 | bootstrapFd, err := os.Open(path) 19 | if err != nil { 20 | return nil, err 21 | } 22 | defer bootstrapFd.Close() 23 | 24 | bootstrapRawData, err := ioutil.ReadAll(bootstrapFd) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | var bsData bootstrapData 30 | err = json.Unmarshal(bootstrapRawData, &bsData) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return &bsData, nil 36 | } 37 | -------------------------------------------------------------------------------- /configstore.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "reflect" 8 | "time" 9 | 10 | v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2" 11 | core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 12 | "github.com/golang/protobuf/ptypes" 13 | "github.com/golang/protobuf/ptypes/any" 14 | v1 "k8s.io/api/core/v1" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/client-go/informers" 17 | "k8s.io/client-go/kubernetes" 18 | "k8s.io/client-go/tools/cache" 19 | "sigs.k8s.io/yaml" 20 | ) 21 | 22 | const ( 23 | ByNodeIdKeyPrefix = "n:" 24 | ByClusterKeyPrefix = "c:" 25 | ) 26 | 27 | type Config struct { 28 | version string 29 | listeners map[string]*v2.Listener 30 | clusters map[string]*v2.Cluster 31 | rules *AssignmentRules 32 | // Set type 33 | services map[string]struct{} 34 | } 35 | 36 | // NewConfig initializes config struct. 37 | func NewConfig() *Config { 38 | return &Config{ 39 | listeners: make(map[string]*v2.Listener), 40 | clusters: make(map[string]*v2.Cluster), 41 | services: make(map[string]struct{}), 42 | } 43 | } 44 | 45 | // Load fills config from config map. 46 | func (config *Config) Load(cm *v1.ConfigMap) error { 47 | config.version = cm.ObjectMeta.ResourceVersion 48 | 49 | listeners, err := extractListeners(cm) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | clusters, err := extractClusters(cm) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | for _, listener := range listeners { 60 | log.Printf("loading listener %s", listener.Name) 61 | config.listeners[listener.Name] = listener 62 | } 63 | 64 | for _, cluster := range clusters { 65 | log.Printf("loading cluster %s", cluster.Name) 66 | if cluster.GetType() == v2.Cluster_EDS { 67 | edsClusterConfig := cluster.EdsClusterConfig 68 | if edsClusterConfig == nil { 69 | d, _ := yaml.Marshal(cluster) 70 | log.Printf("not found expected `eds_cluster_config` section; see parsed YAML:\n\n%s\n", d) 71 | continue 72 | } 73 | 74 | serviceName := edsClusterConfig.ServiceName 75 | if serviceName[:4] == "k8s:" { 76 | serviceName = serviceName[4:] 77 | } 78 | config.services[serviceName] = struct{}{} 79 | } 80 | config.clusters[cluster.Name] = cluster 81 | } 82 | 83 | assignments, err := extractAssignments(cm) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | config.rules = assignments 89 | if err := config.validate(); err != nil { 90 | return err 91 | } 92 | 93 | return nil 94 | 95 | } 96 | 97 | func (c *Config) HasService(name string) bool { 98 | _, ok := c.services[name] 99 | return ok 100 | } 101 | 102 | func (c *Config) getAssignmentCache(node *core.Node) (*assignmentCache, bool) { 103 | a, ok := c.rules.cache[ByNodeIdKeyPrefix+node.Id] 104 | if !ok { 105 | a, ok = c.rules.cache[ByClusterKeyPrefix+node.Cluster] 106 | } 107 | return a, ok 108 | } 109 | 110 | func (c *Config) GetListeners(node *core.Node) ([]byte, bool) { 111 | if cache, ok := c.getAssignmentCache(node); ok { 112 | return cache.listeners, true 113 | } 114 | return nil, false 115 | } 116 | 117 | func (c *Config) GetClusters(node *core.Node) ([]byte, bool) { 118 | if cache, ok := c.getAssignmentCache(node); ok { 119 | return cache.clusters, true 120 | } 121 | return nil, false 122 | } 123 | 124 | func (c *Config) GetClusterNames(node *core.Node) []string { 125 | result := make([]string, 0) 126 | 127 | if assignment, exists := c.rules.ByNodeId[node.Id]; exists { 128 | result = append(result, assignment.Clusters...) 129 | } 130 | 131 | if assignment, exists := c.rules.ByCluster[node.Cluster]; exists { 132 | result = append(result, assignment.Clusters...) 133 | } 134 | 135 | return result 136 | } 137 | 138 | type Assignment struct { 139 | Listeners []string `json:"listeners"` 140 | Clusters []string `json:"clusters"` 141 | } 142 | 143 | type AssignmentRules struct { 144 | ByNodeId map[string]*Assignment `json:"by-node-id"` 145 | ByCluster map[string]*Assignment `json:"by-cluster"` 146 | 147 | cache map[string]*assignmentCache 148 | } 149 | 150 | type assignmentCache struct { 151 | listeners []byte 152 | clusters []byte 153 | } 154 | 155 | type ConfigStore struct { 156 | namespace string 157 | configName string 158 | k8sClient *kubernetes.Clientset 159 | 160 | informer cache.SharedIndexInformer 161 | store cache.Store 162 | 163 | config *Config 164 | configMap *v1.ConfigMap 165 | 166 | lastUpdate time.Time 167 | lastError error 168 | } 169 | 170 | func (cs *ConfigStore) InitFromK8s() error { 171 | namespace, name := k8sSplitName(cs.configName) 172 | log.Println("ConfigStore.Init: ", namespace, name, cs.configName) 173 | cm, err := cs.k8sClient.CoreV1().ConfigMaps(namespace).Get(name, metav1.GetOptions{}) 174 | if err != nil { 175 | log.Println("ConfigStore.Init wups ", err) 176 | return err 177 | } 178 | cs.store.Add(cm) 179 | return cs.Load(cm) 180 | } 181 | 182 | func (cs *ConfigStore) Run() { 183 | cs.informer.Run(nil) 184 | } 185 | 186 | func (cs *ConfigStore) Load(cm *v1.ConfigMap) error { 187 | defer func() { 188 | cs.lastUpdate = time.Now() 189 | }() 190 | configSnapshot := cs.config 191 | cs.config = NewConfig() 192 | if err := cs.config.Load(cm); err != nil { 193 | // Restore previously loaded Config 194 | cs.config = configSnapshot 195 | return err 196 | } 197 | cs.configMap = cm 198 | return nil 199 | } 200 | 201 | func (cs *ConfigStore) GetConfigSnapshot() *Config { 202 | return cs.config 203 | } 204 | 205 | func NewConfigStore( 206 | k8sClient *kubernetes.Clientset, 207 | configName string, 208 | ) *ConfigStore { 209 | cs := &ConfigStore{ 210 | configName: configName, 211 | k8sClient: k8sClient, 212 | } 213 | 214 | namespace, _ := k8sSplitName(configName) 215 | 216 | infFactory := informers.NewSharedInformerFactoryWithOptions(k8sClient, 0, 217 | informers.WithNamespace(namespace), 218 | informers.WithTweakListOptions(func(*metav1.ListOptions) {})) 219 | 220 | cs.informer = infFactory.Core().V1().ConfigMaps().Informer() 221 | cs.store = cs.informer.GetStore() 222 | cs.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ 223 | UpdateFunc: func(old, cur interface{}) { 224 | key, _ := cache.MetaNamespaceKeyFunc(cur) 225 | if key != configName { 226 | return 227 | } 228 | if reflect.DeepEqual(old, cur) { 229 | return 230 | } 231 | 232 | cs.lastError = cs.Load(cur.(*v1.ConfigMap)) 233 | if cs.lastError != nil { 234 | log.Println("update failed: ", cs.lastError) 235 | } else { 236 | log.Println("update applied") 237 | } 238 | }, 239 | }) 240 | return cs 241 | } 242 | 243 | func extractListeners(cm *v1.ConfigMap) ([]*v2.Listener, error) { 244 | // We have to decode our input, which is YAML, so we can iterate 245 | // over each of them. 246 | raw, err := unmarshalYAMLSlice([]byte(cm.Data["listeners"])) 247 | if err != nil { 248 | return nil, errors.New("listeners: invalid YAML: " + err.Error()) 249 | } 250 | rv := make([]*v2.Listener, len(raw)) 251 | for i, r := range raw { 252 | var pb v2.Listener 253 | if err := convertToPb(r, &pb); err != nil { 254 | d, _ := yaml.Marshal(r) 255 | return nil, errors.New(fmt.Sprintf("listeners: index %d: %s:\n\n%s", i, err, d)) 256 | } 257 | rv[i] = &pb 258 | } 259 | return rv, nil 260 | } 261 | 262 | func extractClusters(cm *v1.ConfigMap) ([]*v2.Cluster, error) { 263 | // We have to decode our input, which is YAML, so we can iterate 264 | // over each of them. 265 | raw, err := unmarshalYAMLSlice([]byte(cm.Data["clusters"])) 266 | if err != nil { 267 | return nil, errors.New("clusters: invalid YAML: " + err.Error()) 268 | } 269 | rv := make([]*v2.Cluster, len(raw)) 270 | for i, r := range raw { 271 | var pb v2.Cluster 272 | if err := convertToPb(r, &pb); err != nil { 273 | d, _ := yaml.Marshal(r) 274 | return nil, errors.New(fmt.Sprintf("clusters: index %d: %s:\n\n%s", i, err, d)) 275 | } 276 | rv[i] = &pb 277 | } 278 | return rv, nil 279 | } 280 | 281 | func extractAssignments(cm *v1.ConfigMap) (*AssignmentRules, error) { 282 | var ar AssignmentRules 283 | err := yaml.Unmarshal([]byte(cm.Data["assignments"]), &ar) 284 | return &ar, err 285 | } 286 | 287 | func (config *Config) validate() error { 288 | config.rules.cache = make(map[string]*assignmentCache) 289 | 290 | for key, assignment := range config.rules.ByNodeId { 291 | lr := make([]*any.Any, len(assignment.Listeners)) 292 | cache := &assignmentCache{} 293 | for i, name := range assignment.Listeners { 294 | if listener, ok := config.listeners[name]; !ok { 295 | return errors.New("missing listener: " + name) 296 | } else { 297 | r, _ := ptypes.MarshalAny(listener) 298 | lr[i] = r 299 | } 300 | } 301 | cache.listeners, _ = structToJSON(&v2.DiscoveryResponse{ 302 | VersionInfo: config.version, 303 | Resources: lr, 304 | }) 305 | 306 | cr := make([]*any.Any, len(assignment.Clusters)) 307 | for i, name := range assignment.Clusters { 308 | if cluster, ok := config.clusters[name]; !ok { 309 | return errors.New("unknown cluster: " + name) 310 | } else { 311 | r, _ := ptypes.MarshalAny(cluster) 312 | cr[i] = r 313 | } 314 | } 315 | cache.clusters, _ = structToJSON(&v2.DiscoveryResponse{ 316 | VersionInfo: config.version, 317 | Resources: cr, 318 | }) 319 | 320 | config.rules.cache[ByNodeIdKeyPrefix+key] = cache 321 | } 322 | 323 | for key, assignment := range config.rules.ByCluster { 324 | lr := make([]*any.Any, len(assignment.Listeners)) 325 | cache := &assignmentCache{} 326 | for i, name := range assignment.Listeners { 327 | if listener, ok := config.listeners[name]; !ok { 328 | return errors.New("missing listener: " + name) 329 | } else { 330 | r, _ := ptypes.MarshalAny(listener) 331 | lr[i] = r 332 | } 333 | } 334 | cache.listeners, _ = structToJSON(&v2.DiscoveryResponse{ 335 | VersionInfo: config.version, 336 | Resources: lr, 337 | }) 338 | 339 | cr := make([]*any.Any, len(assignment.Clusters)) 340 | for i, name := range assignment.Clusters { 341 | if cluster, ok := config.clusters[name]; !ok { 342 | return errors.New("unknown cluster: " + name) 343 | } else { 344 | r, _ := ptypes.MarshalAny(cluster) 345 | cr[i] = r 346 | } 347 | } 348 | cache.clusters, _ = structToJSON(&v2.DiscoveryResponse{ 349 | VersionInfo: config.version, 350 | Resources: cr, 351 | }) 352 | 353 | config.rules.cache[ByClusterKeyPrefix+key] = cache 354 | } 355 | return nil 356 | } 357 | -------------------------------------------------------------------------------- /controller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "k8s.io/client-go/kubernetes" 5 | ) 6 | 7 | type Controller struct { 8 | k8sClient *kubernetes.Clientset 9 | 10 | configStore *ConfigStore 11 | epStore *EpStore 12 | } 13 | 14 | func NewController( 15 | k8sClient *kubernetes.Clientset, 16 | configName string, 17 | ) *Controller { 18 | c := &Controller{ 19 | k8sClient: k8sClient, 20 | configStore: NewConfigStore(k8sClient, configName), 21 | } 22 | 23 | if err := c.configStore.InitFromK8s(); err != nil { 24 | panic(err) 25 | } 26 | 27 | c.epStore = NewEpStore(k8sClient, c.configStore) 28 | if err := c.epStore.Init(); err != nil { 29 | panic(err) 30 | } 31 | return c 32 | } 33 | 34 | func (c *Controller) Run() { 35 | go c.configStore.Run() 36 | go c.epStore.Run() 37 | } 38 | 39 | func (c *Controller) GetEndpoints(cluster string) (*Endpoints, bool) { 40 | return c.epStore.Get(cluster) 41 | } 42 | 43 | func (c *Controller) GetConfigSnapshot() *Config { 44 | return c.configStore.GetConfigSnapshot() 45 | } 46 | -------------------------------------------------------------------------------- /envoy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "strconv" 9 | ) 10 | 11 | // /etc/envoy/envoy.yaml 12 | const ENVOY_BOOTSTRAP_CONFIG = ` 13 | static_resources: 14 | clusters: 15 | - name: xds_cluster 16 | type: LOGICAL_DNS 17 | connect_timeout: 0.5s 18 | load_assignment: 19 | cluster_name: xds_cluster 20 | endpoints: 21 | - lb_endpoints: 22 | - endpoint: 23 | address: 24 | socket_address: 25 | address: %s 26 | port_value: 80 27 | 28 | dynamic_resources: 29 | lds_config: 30 | api_config_source: 31 | api_type: REST 32 | cluster_names: [xds_cluster] 33 | refresh_delay: 3600s 34 | request_timeout: 1s 35 | 36 | cds_config: 37 | api_config_source: 38 | api_type: REST 39 | cluster_names: [xds_cluster] 40 | refresh_delay: 3600s 41 | request_timeout: 1s 42 | ` 43 | 44 | func runEnvoy(serviceNode, serviceCluster, endpoint, envoyConfigPath string, concurrency int) error { 45 | f, err := os.Create(envoyConfigPath) 46 | if err != nil { 47 | return err 48 | } 49 | f.WriteString(buildEnvoyBootstrapConfig(endpoint)) 50 | f.Close() 51 | 52 | envoyCommand := exec.Command( 53 | "envoy", 54 | "--service-node", serviceNode, 55 | "--service-cluster", serviceCluster, 56 | "--concurrency", strconv.Itoa(concurrency), 57 | "-c", envoyConfigPath, 58 | ) 59 | 60 | envoyCommand.Stdout = os.Stdout 61 | envoyCommand.Stderr = os.Stderr 62 | 63 | err = envoyCommand.Start() 64 | if err != nil { 65 | return err 66 | } 67 | 68 | go func() { 69 | envoyCommand.Wait() 70 | log.Printf("Envoy subprocess exited, closing parent process") 71 | os.Exit(-1) 72 | }() 73 | 74 | return nil 75 | } 76 | 77 | func buildEnvoyBootstrapConfig(endpoint string) string { 78 | return fmt.Sprintf(ENVOY_BOOTSTRAP_CONFIG, endpoint) 79 | } 80 | -------------------------------------------------------------------------------- /epstore.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | "sync" 7 | 8 | v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2" 9 | core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 10 | endpoint "github.com/envoyproxy/go-control-plane/envoy/api/v2/endpoint" 11 | "github.com/golang/protobuf/ptypes" 12 | "github.com/golang/protobuf/ptypes/any" 13 | v1 "k8s.io/api/core/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/client-go/informers" 16 | "k8s.io/client-go/kubernetes" 17 | "k8s.io/client-go/tools/cache" 18 | ) 19 | 20 | type EpStore struct { 21 | k8sClient *kubernetes.Clientset 22 | 23 | informer cache.SharedIndexInformer 24 | store cache.Store 25 | 26 | configStore *ConfigStore 27 | 28 | registry sync.Map 29 | } 30 | 31 | type Endpoints struct { 32 | version string 33 | data []byte 34 | } 35 | 36 | func NewEpStore( 37 | k8sClient *kubernetes.Clientset, 38 | configStore *ConfigStore, 39 | ) *EpStore { 40 | es := &EpStore{ 41 | k8sClient: k8sClient, 42 | configStore: configStore, 43 | } 44 | infFactory := informers.NewSharedInformerFactoryWithOptions(k8sClient, 0, 45 | informers.WithTweakListOptions(func(*metav1.ListOptions) {})) 46 | 47 | es.informer = infFactory.Core().V1().Endpoints().Informer() 48 | es.store = es.informer.GetStore() 49 | es.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ 50 | AddFunc: func(obj interface{}) { 51 | config := es.configStore.GetConfigSnapshot() 52 | 53 | key, _ := cache.MetaNamespaceKeyFunc(obj) 54 | if !config.HasService(key) { 55 | return 56 | } 57 | 58 | es.LoadEp(obj.(*v1.Endpoints)) 59 | }, 60 | UpdateFunc: func(old, cur interface{}) { 61 | config := es.configStore.GetConfigSnapshot() 62 | 63 | key, _ := cache.MetaNamespaceKeyFunc(cur) 64 | if !config.HasService(key) { 65 | return 66 | } 67 | oep := old.(*v1.Endpoints) 68 | cep := cur.(*v1.Endpoints) 69 | 70 | if reflect.DeepEqual(cep.Subsets, oep.Subsets) { 71 | return 72 | } 73 | 74 | es.LoadEp(cep) 75 | }, 76 | DeleteFunc: func(obj interface{}) { 77 | config := es.configStore.GetConfigSnapshot() 78 | 79 | key, _ := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) 80 | if !config.HasService(key) { 81 | return 82 | } 83 | 84 | es.DeleteEp(key) 85 | }, 86 | }) 87 | return es 88 | } 89 | 90 | func (es *EpStore) Init() error { 91 | config := es.configStore.GetConfigSnapshot() 92 | eps, err := es.k8sClient.CoreV1().Endpoints(v1.NamespaceAll).List(metav1.ListOptions{}) 93 | if err != nil { 94 | return err 95 | } 96 | for _, ep := range eps.Items { 97 | if !config.HasService(ep.GetNamespace() + "/" + ep.GetName()) { 98 | continue 99 | } 100 | es.store.Add(&ep) 101 | es.LoadEp(&ep) 102 | } 103 | return nil 104 | } 105 | 106 | func (es *EpStore) Run() { 107 | es.informer.Run(nil) 108 | } 109 | 110 | func validSubset(subset v1.EndpointSubset) bool { 111 | return len(subset.Ports) == 1 112 | } 113 | 114 | func (es *EpStore) LoadEp(ep *v1.Endpoints) { 115 | epKey := ep.GetNamespace() + "/" + ep.GetName() 116 | version := ep.ObjectMeta.ResourceVersion 117 | 118 | // Check if the existing resource version is the same 119 | if ep, ok := es.registry.Load(epKey); ok && ep.(*Endpoints).version == version { 120 | return 121 | } 122 | 123 | // Count how many registrations we need here 124 | // so that we can correctly allocate what we need 125 | n := 0 126 | for _, subset := range ep.Subsets { 127 | if !validSubset(subset) { 128 | continue 129 | } 130 | n += len(subset.Addresses) 131 | } 132 | 133 | cla := &v2.ClusterLoadAssignment{ 134 | ClusterName: epKey, 135 | Endpoints: []*endpoint.LocalityLbEndpoints{{ 136 | LbEndpoints: make([]*endpoint.LbEndpoint, n), 137 | }}, 138 | } 139 | 140 | n = 0 141 | for _, subset := range ep.Subsets { 142 | if !validSubset(subset) { 143 | continue 144 | } 145 | for _, address := range subset.Addresses { 146 | 147 | cla.Endpoints[0].LbEndpoints[n] = &endpoint.LbEndpoint{ 148 | HostIdentifier: &endpoint.LbEndpoint_Endpoint{ 149 | Endpoint: &endpoint.Endpoint{ 150 | Address: &core.Address{ 151 | Address: &core.Address_SocketAddress{ 152 | SocketAddress: &core.SocketAddress{ 153 | Protocol: core.SocketAddress_TCP, 154 | Address: address.IP, 155 | PortSpecifier: &core.SocketAddress_PortValue{ 156 | PortValue: uint32(subset.Ports[0].Port), 157 | }, 158 | }, 159 | }, 160 | }, 161 | }, 162 | }, 163 | } 164 | log.Printf("%s/%s:%d\n", ep.GetName(), address.IP, subset.Ports[0].Port) 165 | n++ 166 | } 167 | } 168 | 169 | r, _ := ptypes.MarshalAny(cla) 170 | j, _ := structToJSON(&v2.DiscoveryResponse{ 171 | VersionInfo: version, 172 | Resources: []*any.Any{r}, 173 | }) 174 | 175 | // Write entire DiscoveryResponse into the registry 176 | es.registry.Store(epKey, &Endpoints{ 177 | version: version, 178 | data: j, 179 | }) 180 | } 181 | 182 | func (es *EpStore) DeleteEp(key string) { 183 | log.Println("removing service: " + key) 184 | es.registry.Delete(key) 185 | } 186 | 187 | func (es *EpStore) Get(key string) (*Endpoints, bool) { 188 | // HACK: Strip an old k8s prefix 189 | if key[:4] == "k8s:" { 190 | key = key[4:] 191 | } 192 | if ep, ok := es.registry.Load(key); ok { 193 | return ep.(*Endpoints), true 194 | } 195 | return nil, false 196 | } 197 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # xDS Example 2 | 3 | This example can be used for local development on the xDS project. Contains 4 | minimal configuration for deployment to k8s and envoy configuration that uses 5 | the xDS service. 6 | 7 | ## Requirements 8 | 9 | - access to a (local) k8s cluster 10 | - docker for building the xDS service container 11 | - envoy binary or container 12 | 13 | For running tests you will require besides above mentioned also: 14 | 15 | - curl, jq 16 | - bats - the bash tests runner 17 | 18 | ## Build the xds docker image 19 | 20 | From root of the repository: 21 | 22 | ```bash 23 | docker build -t xds -f Dockerfile . 24 | ``` 25 | 26 | ## Deploy xDS to k8s 27 | 28 | The `XDS_CONFIGMAP` environment variable points to the used config map. It is 29 | in form `{namespace}/{configmap name}`. In this example it is part of the 30 | `deployment.yaml` and defaults to `default/xds`. 31 | 32 | Deploy configmap and the xds deployment, envoy and testing service from the 33 | `example/k8s` sub directory: 34 | 35 | ```bash 36 | kubectl apply -f k8s/configmap.yaml -f k8s/xds.yaml 37 | ``` 38 | 39 | ## Access xds service 40 | 41 | Assuming local k8s cluster is used which expose `NodePort` services on 42 | localhost. 43 | 44 | Retrieve information about the port used by the xds service: 45 | 46 | ```bash 47 | kubectl get service xds 48 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 49 | xds NodePort 10.104.220.250 80:30311/TCP 2s 50 | ``` 51 | 52 | Curl the xDS service using the node port `30311`: 53 | 54 | ```bash 55 | curl localhost:30311/config 56 | {"version":"17287","last_error":"","last_update":"2020-05-13T11:38:45.3721989Z"} 57 | ``` 58 | 59 | ```bash 60 | curl localhost:30311/healthz 61 | ok 62 | ``` 63 | 64 | 65 | ## Access xds pod 66 | 67 | Get name of the running xds pod: 68 | 69 | ```bash 70 | kubectl get pods 71 | NAME READY STATUS RESTARTS AGE 72 | xds-f68c64b47-95p9s 0/1 CrashLoopBackOff 15 3h8m 73 | ``` 74 | 75 | Forward its port to the localhost: 76 | 77 | ```bash 78 | kubectl port-forward xds-f68c64b47-95p9s 8080:80 79 | Forwarding from 127.0.0.1:8080 -> 80 80 | Forwarding from [::1]:8080 -> 80 81 | ``` 82 | 83 | Check that it is running and reachable: 84 | 85 | ```bash 86 | curl localhost:8080/config 87 | 88 | {"version":"14120","last_error":"","last_update":"2020-05-13T11:08:34.2465368Z"} 89 | ``` 90 | 91 | ```bash 92 | curl localhost:8080/healthz 93 | 94 | ok 95 | ``` 96 | 97 | Port forwarding is usefull when one wants to access specific pod but it 98 | becomes annoying when pushing new images and creating new pods which requires 99 | to also recreate the port forwarding. 100 | 101 | 102 | # Whole example environment 103 | 104 | In the `example/k8s` is configuration to spin whole, most basic, integration 105 | testing environment consisting of: 106 | 107 | - xDS 108 | - envoy - configured to use xDS 109 | - foo, bar - services discoverable by envoy through xDS 110 | 111 | ## Build images 112 | 113 | From root of this repository: 114 | 115 | ``` 116 | docker build -t xds . 117 | docker build -t xdsenvoy example/envoy 118 | ``` 119 | 120 | Envoy's bootstrap configuration is in `example/envoy/envoy.yaml`. 121 | 122 | ### Deploy & Delete 123 | 124 | ``` 125 | kubectl apply -f example/k8s/ 126 | ``` 127 | 128 | This will deploy set of deplyments and their respective services. 129 | 130 | ``` 131 | kubectl get services 132 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 133 | bar NodePort 10.106.164.147 80:30798/TCP 5s 134 | envoy NodePort 10.107.243.120 8001:32075/TCP,10001:31763/TCP 5s 135 | foo NodePort 10.98.250.222 80:32335/TCP 5s 136 | kubernetes ClusterIP 10.96.0.1 443/TCP 2d3h 137 | xds NodePort 10.106.127.94 80:32344/TCP 8m37s 138 | ``` 139 | 140 | To delete: 141 | 142 | ``` 143 | kubectl delete -f example/k8s/ 144 | ``` 145 | 146 | 147 | # Running test suite 148 | 149 | The test suite will deploy the whole ensemble, check basic functionality and 150 | delete all the k8s resources afterwards. Please install `bats` (`brew install bats`) 151 | tests runner. 152 | 153 | ``` 154 | ./runtests.sh 155 | ``` 156 | -------------------------------------------------------------------------------- /example/envoy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM envoyproxy/envoy:v1.16.0 2 | COPY envoy.yaml /etc/envoy/envoy.yaml 3 | RUN mkdir -p /run/envoy 4 | -------------------------------------------------------------------------------- /example/envoy/envoy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | node: 3 | id: foo 4 | cluster: foo 5 | 6 | static_resources: 7 | clusters: 8 | - name: xds_cluster 9 | type: LOGICAL_DNS 10 | connect_timeout: 0.5s 11 | load_assignment: 12 | cluster_name: xds_cluster 13 | endpoints: 14 | - lb_endpoints: 15 | - endpoint: 16 | address: 17 | socket_address: 18 | address: xds.default.svc.cluster.local 19 | port_value: 80 20 | 21 | dynamic_resources: 22 | lds_config: 23 | api_config_source: 24 | api_type: REST 25 | cluster_names: [xds_cluster] 26 | refresh_delay: 5s 27 | request_timeout: 1s 28 | cds_config: 29 | api_config_source: 30 | api_type: REST 31 | cluster_names: [xds_cluster] 32 | refresh_delay: 5s 33 | request_timeout: 1s 34 | 35 | 36 | admin: 37 | access_log_path: "/dev/null" 38 | address: 39 | socket_address: 40 | address: 0.0.0.0 41 | port_value: 8001 42 | -------------------------------------------------------------------------------- /example/k8s/configmap.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: xds 6 | data: 7 | listeners: | 8 | - name: foo 9 | address: 10 | socket_address: 11 | address: 0.0.0.0 12 | port_value: 10001 13 | filter_chains: 14 | - filters: 15 | - name: envoy.tcp_proxy 16 | typed_config: 17 | '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy 18 | cluster: foo 19 | stat_prefix: foo 20 | - name: envoy.filters.http.health_check 21 | typed_config: 22 | "@type": type.googleapis.com/envoy.extensions.filters.http.health_check.v3.HealthCheck 23 | pass_through_mode: true 24 | cache_time: 1s 25 | headers: 26 | - name: :path 27 | exact_match: /health/ 28 | - name: bar 29 | address: 30 | socket_address: 31 | address: 0.0.0.0 32 | port_value: 10002 33 | filter_chains: 34 | - filters: 35 | - name: envoy.tcp_proxy 36 | typed_config: 37 | '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy 38 | cluster: bar 39 | stat_prefix: bar 40 | - name: qux 41 | address: 42 | socket_address: 43 | address: 0.0.0.0 44 | port_value: 10002 45 | filter_chains: 46 | - filters: 47 | - name: envoy.tcp_proxy 48 | typed_config: 49 | '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy 50 | cluster: qux 51 | stat_prefix: qux 52 | 53 | clusters: | 54 | - name: foo 55 | type: EDS 56 | connect_timeout: 0.25s 57 | eds_cluster_config: 58 | service_name: default/foo 59 | eds_config: 60 | api_config_source: 61 | api_type: REST 62 | cluster_names: [xds_cluster] 63 | refresh_delay: 1s 64 | - name: bar 65 | type: EDS 66 | connect_timeout: 0.25s 67 | eds_cluster_config: 68 | service_name: default/bar 69 | eds_config: 70 | api_config_source: 71 | api_type: REST 72 | cluster_names: [xds_cluster] 73 | refresh_delay: 1s 74 | - name: baz 75 | type: STATIC 76 | connect_timeout: 0.25s 77 | load_assignment: 78 | cluster_name: baz 79 | endpoints: 80 | - lb_endpoints: 81 | - endpoint: 82 | address: 83 | socket_address: 84 | address: 127.0.0.1 85 | port_value: 8888 86 | - name: qux 87 | type: EDS 88 | connect_timeout: 0.25s 89 | eds_cluster_config: 90 | service_name: quxns/qux 91 | eds_config: 92 | api_config_source: 93 | api_type: REST 94 | cluster_names: [xds_cluster] 95 | refresh_delay: 1s 96 | 97 | assignments: | 98 | by-cluster: 99 | foo: 100 | listeners: 101 | - foo 102 | clusters: 103 | - foo 104 | bar: 105 | listeners: 106 | - bar 107 | clusters: 108 | - bar 109 | qux: 110 | listeners: 111 | - qux 112 | clusters: 113 | - qux 114 | baz: 115 | clusters: 116 | - bar 117 | - baz 118 | by-node-id: 119 | foo: 120 | listeners: 121 | - foo 122 | clusters: 123 | - foo 124 | bar: 125 | listeners: 126 | - bar 127 | clusters: 128 | - bar 129 | qux: 130 | listeners: 131 | - qux 132 | clusters: 133 | - qux 134 | -------------------------------------------------------------------------------- /example/k8s/envoy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: envoy 6 | labels: 7 | service: envoy 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | service: envoy 13 | template: 14 | metadata: 15 | labels: 16 | service: envoy 17 | spec: 18 | containers: 19 | - image: xdsenvoy 20 | imagePullPolicy: Never 21 | name: envoy 22 | --- 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: envoy 27 | spec: 28 | type: NodePort 29 | selector: 30 | service: envoy 31 | ports: 32 | - port: 8001 33 | name: admin 34 | - port: 10001 35 | name: foo 36 | -------------------------------------------------------------------------------- /example/k8s/foobar.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: foo 6 | labels: 7 | service: foo 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | service: foo 13 | template: 14 | metadata: 15 | labels: 16 | service: foo 17 | spec: 18 | containers: 19 | - image: nginxdemos/hello:plain-text 20 | name: foo 21 | --- 22 | apiVersion: v1 23 | kind: Service 24 | metadata: 25 | name: foo 26 | spec: 27 | type: NodePort 28 | selector: 29 | service: foo 30 | ports: 31 | - protocol: TCP 32 | port: 80 33 | --- 34 | apiVersion: apps/v1 35 | kind: Deployment 36 | metadata: 37 | name: bar 38 | labels: 39 | service: bar 40 | spec: 41 | replicas: 1 42 | selector: 43 | matchLabels: 44 | service: bar 45 | template: 46 | metadata: 47 | labels: 48 | service: bar 49 | spec: 50 | containers: 51 | - image: nginxdemos/hello:plain-text 52 | name: bar 53 | --- 54 | apiVersion: v1 55 | kind: Service 56 | metadata: 57 | name: bar 58 | spec: 59 | type: NodePort 60 | selector: 61 | service: bar 62 | ports: 63 | - protocol: TCP 64 | port: 80 65 | --- 66 | apiVersion: v1 67 | kind: Namespace 68 | metadata: 69 | name: quxns 70 | --- 71 | apiVersion: apps/v1 72 | kind: Deployment 73 | metadata: 74 | name: qux 75 | namespace: quxns 76 | labels: 77 | service: qux 78 | spec: 79 | replicas: 1 80 | selector: 81 | matchLabels: 82 | service: qux 83 | template: 84 | metadata: 85 | labels: 86 | service: qux 87 | spec: 88 | containers: 89 | - image: nginxdemos/hello:plain-text 90 | name: qux 91 | --- 92 | apiVersion: v1 93 | kind: Service 94 | metadata: 95 | name: qux 96 | namespace: quxns 97 | spec: 98 | type: NodePort 99 | selector: 100 | service: qux 101 | ports: 102 | - protocol: TCP 103 | port: 80 104 | -------------------------------------------------------------------------------- /example/k8s/xds.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: xds 6 | labels: 7 | service: xds 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | service: xds 13 | template: 14 | metadata: 15 | labels: 16 | service: xds 17 | spec: 18 | containers: 19 | - image: xds 20 | imagePullPolicy: Never 21 | name: xds 22 | env: 23 | - name: GOMAXPROCS 24 | value: "1" 25 | - name: XDS_LISTEN 26 | value: "0.0.0.0:80" 27 | - name: XDS_CONFIGMAP 28 | value: default/xds 29 | --- 30 | apiVersion: v1 31 | kind: Service 32 | metadata: 33 | name: xds 34 | spec: 35 | type: NodePort 36 | selector: 37 | service: xds 38 | ports: 39 | - protocol: TCP 40 | port: 80 41 | -------------------------------------------------------------------------------- /example/runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | SCRIPT_DIR=$(realpath "$(dirname "$0")") 6 | XDS_DIR=$(dirname "${SCRIPT_DIR}") 7 | 8 | function log { 9 | echo "[$(date '+%Y-%m-%d %H:%M:%S')]: $1" 10 | } 11 | 12 | function check_kubectl_context { 13 | local k8s_ctx 14 | k8s_ctx=$(kubectl config current-context) 15 | echo "Test runner creates/deletes resources using kubectl" 16 | echo "Current kubectl context: ${k8s_ctx}" 17 | read -rp "Continue (y/n)? " choice 18 | case "$choice" in 19 | y|Y ) echo;; 20 | * ) exit 1;; 21 | esac 22 | } 23 | 24 | function main { 25 | 26 | check_kubectl_context 27 | 28 | log "Building xds docker image" 29 | docker build -t xds "${XDS_DIR}" 30 | 31 | log "Building envoy image" 32 | docker build -t xdsenvoy "${SCRIPT_DIR}/envoy" 33 | 34 | 35 | log "Deploy to k8s" 36 | kubectl apply -f "${SCRIPT_DIR}/k8s" 37 | trap 'kubectl delete -f "${SCRIPT_DIR}/k8s"' exit 38 | 39 | log "Wait a bit ..." 40 | sleep 5 41 | 42 | log "Restart envoy to pick up endpoints correctly" 43 | kubectl scale deployment envoy --replicas 0 44 | kubectl scale deployment envoy --replicas 1 45 | 46 | log "Wait a bit ..." 47 | sleep 5 48 | 49 | local xds_port; 50 | local envoy_admin_port; 51 | local envoy_foo_port; 52 | 53 | xds_port=$(kubectl get service xds -o json | jq '.spec.ports[0].nodePort') 54 | envoy_admin_port=$(kubectl get service envoy -o json | jq '.spec.ports[] | select(.name=="admin") | .nodePort') 55 | envoy_foo_port=$(kubectl get service envoy -o json | jq '.spec.ports[] | select(.name=="foo") | .nodePort') 56 | 57 | log "Running tests" 58 | XDS_URL="http://localhost:${xds_port}" \ 59 | ENVOY_ADMIN_URL="http://localhost:${envoy_admin_port}" \ 60 | ENVOY_FOO_URL="http://localhost:${envoy_foo_port}" \ 61 | SCRIPT_DIR="${SCRIPT_DIR}" \ 62 | bats tests/ 63 | 64 | } 65 | 66 | main "$@" 67 | -------------------------------------------------------------------------------- /example/tests/envoy.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "/config_dump contains dynamicly configured foo cluster" { 4 | result="$( 5 | curl -f -s "${ENVOY_ADMIN_URL}/config_dump" | jq \ 6 | '.configs[] 7 | | select(.dynamic_active_clusters != null) 8 | | .dynamic_active_clusters[0].cluster.name' 9 | )" 10 | [ "$result" == '"foo"' ] 11 | } 12 | 13 | @test "foo cluster response is from foo server" { 14 | curl -f -s "${ENVOY_FOO_URL}" | grep "Server name: foo-" 15 | } 16 | -------------------------------------------------------------------------------- /example/tests/xds.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "/heatlhz endpoint is ok" { 4 | result="$(curl -f -s "${XDS_URL}/healthz")" 5 | [ "$result" == "ok" ] 6 | } 7 | 8 | @test "/config endpoint reports no error" { 9 | result="$(curl -f -s "${XDS_URL}/config" | jq '.last_error')" 10 | [ "$result" == '""' ] 11 | } 12 | 13 | @test "/v2/discovery:clusters select foo" { 14 | run curl -s -X POST \ 15 | -d '{"node": {"id": "", "cluster":"foo"}}' \ 16 | "${XDS_URL}/v2/discovery:clusters" 17 | 18 | [ "${status}" -eq 0 ] 19 | [ "$(echo "${output}" | jq '.resources | length')" -eq 1 ] 20 | [ "$(echo "${output}" | jq '.resources[0].name')" == '"foo"' ] 21 | 22 | } 23 | 24 | @test "/v2/discovery:clusters select bar" { 25 | run curl -s -X POST \ 26 | -d '{"node": {"id": "", "cluster":"bar"}}' \ 27 | "${XDS_URL}/v2/discovery:clusters" 28 | 29 | [ "${status}" -eq 0 ] 30 | [ "$(echo "${output}" | jq '.resources | length')" -eq 1 ] 31 | [ "$(echo "${output}" | jq '.resources[0].name')" == '"bar"' ] 32 | 33 | } 34 | 35 | @test "/v2/discovery:clusters select baz" { 36 | run curl -s -X POST \ 37 | -d '{"node": {"id": "", "cluster":"baz"}}' \ 38 | "${XDS_URL}/v2/discovery:clusters" 39 | 40 | 41 | [ "${status}" -eq 0 ] 42 | [ "$(echo "${output}" | jq '.resources | length')" -eq 2 ] 43 | [ "$(echo "${output}" | jq '.resources[0].name')" == '"bar"' ] 44 | [ "$(echo "${output}" | jq '.resources[1].name')" == '"baz"' ] 45 | [ "$(echo "${output}" | jq '.resources[1].type')" == '"STATIC"' ] 46 | [ "$(echo "${output}" | jq '.resources[1].load_assignment.endpoints[0].lb_endpoints[0].endpoint.address.socket_address.port_value')" -eq 8888 ] 47 | 48 | } 49 | 50 | @test "/v2/discovery:clusters select qux" { 51 | run curl -s -X POST \ 52 | -d '{"node": {"id": "", "cluster":"qux"}}' \ 53 | "${XDS_URL}/v2/discovery:clusters" 54 | 55 | [ "${status}" -eq 0 ] 56 | [ "$(echo "${output}" | jq '.resources | length')" -eq 1 ] 57 | [ "$(echo "${output}" | jq '.resources[0].name')" == '"qux"' ] 58 | 59 | } 60 | 61 | @test "/v2/discovery:listeners select foo" { 62 | run curl -s -X POST \ 63 | -d '{"node": {"id": "", "cluster":"foo"}}' \ 64 | "${XDS_URL}/v2/discovery:listeners" 65 | 66 | [ "${status}" -eq 0 ] 67 | [ "$(echo "${output}" | jq '.resources | length')" -eq 1 ] 68 | [ "$(echo "${output}" | jq '.resources[0].name')" == '"foo"' ] 69 | 70 | } 71 | 72 | @test "/v2/discovery:listeners select bar" { 73 | run curl -s -X POST \ 74 | -d '{"node": {"id": "", "cluster":"bar"}}' \ 75 | "${XDS_URL}/v2/discovery:listeners" 76 | 77 | [ "${status}" -eq 0 ] 78 | [ "$(echo "${output}" | jq '.resources | length')" -eq 1 ] 79 | [ "$(echo "${output}" | jq '.resources[0].name')" == '"bar"' ] 80 | 81 | } 82 | 83 | @test "/v2/discovery:listeners select qux" { 84 | run curl -s -X POST \ 85 | -d '{"node": {"id": "", "cluster":"qux"}}' \ 86 | "${XDS_URL}/v2/discovery:listeners" 87 | 88 | [ "${status}" -eq 0 ] 89 | [ "$(echo "${output}" | jq '.resources | length')" -eq 1 ] 90 | [ "$(echo "${output}" | jq '.resources[0].name')" == '"qux"' ] 91 | 92 | } 93 | 94 | @test "/v2/discovery:endpoints select foo" { 95 | run curl -s -X POST \ 96 | -d '{"node": {"id": "", "cluster":"foo"}, "resource_names": ["default/foo"]}' \ 97 | "${XDS_URL}/v2/discovery:endpoints" 98 | 99 | [ "${status}" -eq 0 ] 100 | [ "$(echo "${output}" | jq '.resources | length')" -eq 1 ] 101 | [ "$(echo "${output}" | jq '.resources[0].cluster_name')" == '"default/foo"' ] 102 | 103 | } 104 | 105 | @test "/v2/discovery:endpoints select bar" { 106 | run curl -s -X POST \ 107 | -d '{"node": {"id": "", "cluster":"bar"}, "resource_names": ["default/bar"]}' \ 108 | "${XDS_URL}/v2/discovery:endpoints" 109 | 110 | [ "${status}" -eq 0 ] 111 | [ "$(echo "${output}" | jq '.resources | length')" -eq 1 ] 112 | [ "$(echo "${output}" | jq '.resources[0].cluster_name')" == '"default/bar"' ] 113 | 114 | } 115 | 116 | @test "/v2/discovery:endpoints select qux" { 117 | run curl -s -X POST \ 118 | -d '{"node": {"id": "", "cluster":"qux"}, "resource_names": ["quxns/qux"]}' \ 119 | "${XDS_URL}/v2/discovery:endpoints" 120 | 121 | echo "${output}" >> /tmp/quxout 122 | 123 | [ "${status}" -eq 0 ] 124 | [ "$(echo "${output}" | jq '.resources | length')" -eq 1 ] 125 | [ "$(echo "${output}" | jq '.resources[0].cluster_name')" == '"quxns/qux"' ] 126 | 127 | } 128 | 129 | @test "/validate endpoint invalid configmap" { 130 | run curl -s -f -d "invalid yaml" "${XDS_URL}/validate" 131 | [ "${status}" -ne 0 ] 132 | } 133 | 134 | @test "/validate endpoint valid configmap" { 135 | run curl -s -f --data-binary @"${SCRIPT_DIR}/k8s/configmap.yaml" "${XDS_URL}/validate" 136 | [ "${status}" -eq 0 ] 137 | [ "${output}" == "ok" ] 138 | } 139 | 140 | 141 | @test "--validate from stdin invalid" { 142 | run docker run --rm -i xds --validate - <<<"invalid yaml" 143 | [ "${status}" -eq 1 ] 144 | } 145 | 146 | @test "--validate from stdin valid" { 147 | docker run --rm -i xds --validate - < "${SCRIPT_DIR}/k8s/configmap.yaml" 148 | } 149 | 150 | @test "--validate file invalid" { 151 | run docker run --rm -i xds --validate <(echo "invalid") 152 | [ "${status}" -eq 1 ] 153 | } 154 | 155 | @test "--validate file valid" { 156 | docker run --volume "${SCRIPT_DIR}/k8s/configmap.yaml:/configmap.yaml" --rm -i xds --validate /configmap.yaml 157 | } 158 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module xds 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/envoyproxy/go-control-plane v0.9.8 8 | github.com/ghodss/yaml v1.0.0 // indirect 9 | github.com/gogo/protobuf v1.3.2 // indirect 10 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect 11 | github.com/golang/protobuf v1.4.2 12 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect 13 | github.com/google/gofuzz v1.0.0 // indirect 14 | github.com/googleapis/gnostic v0.2.0 // indirect 15 | github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect 16 | github.com/hashicorp/go-multierror v1.0.0 17 | github.com/hashicorp/golang-lru v0.5.0 // indirect 18 | github.com/imdario/mergo v0.3.7 // indirect 19 | github.com/json-iterator/go v1.1.6 // indirect 20 | github.com/mitchellh/go-homedir v1.1.0 21 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 22 | github.com/modern-go/reflect2 v1.0.1 // indirect 23 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 24 | github.com/spf13/pflag v1.0.3 // indirect 25 | github.com/stretchr/testify v1.6.1 // indirect 26 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect 27 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect 28 | gopkg.in/inf.v0 v0.9.1 // indirect 29 | k8s.io/api v0.0.0-20181121071145-b7bd5f2d334c 30 | k8s.io/apimachinery v0.0.0-20181126122622-195a1699ff5c 31 | k8s.io/client-go v9.0.0+incompatible 32 | k8s.io/klog v0.3.2 33 | sigs.k8s.io/yaml v1.1.0 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= 6 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 7 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 8 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= 9 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 14 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 15 | github.com/envoyproxy/go-control-plane v0.9.8 h1:bbmjRkjmP0ZggMoahdNMmJFFnK7v5H+/j5niP5QH6bg= 16 | github.com/envoyproxy/go-control-plane v0.9.8/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 17 | github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= 18 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 19 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 20 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 21 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 22 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 23 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 24 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 25 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= 26 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 27 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 28 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 29 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 31 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 32 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 33 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 34 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 35 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 36 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 37 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 38 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= 39 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 40 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 41 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 42 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 43 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 44 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 45 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 46 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 47 | github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= 48 | github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 49 | github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q= 50 | github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 51 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 52 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 53 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 54 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 55 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 56 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 57 | github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= 58 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 59 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= 60 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 61 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 62 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 63 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 64 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 65 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 66 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 67 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 68 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 69 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 70 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 71 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 72 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 73 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 74 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 75 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 76 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 77 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 78 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 79 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 80 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 81 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 82 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 83 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 84 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 85 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 86 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 87 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 88 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 89 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 90 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 91 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 92 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 93 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 94 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 95 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 96 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 97 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 98 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 99 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 100 | golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= 101 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 102 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 103 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 104 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 105 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 109 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 111 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 112 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 113 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 114 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 115 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= 117 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 119 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 120 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 121 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 122 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 123 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 124 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 125 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 126 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 127 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 128 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 129 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 130 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 131 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 132 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 133 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 135 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 136 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 137 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 138 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 139 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 140 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 141 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= 142 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 143 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= 144 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 145 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 146 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 147 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 148 | google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= 149 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 150 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 151 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 152 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 153 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 154 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 155 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 156 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 157 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 158 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 159 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 160 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 161 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 162 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 163 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 164 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 165 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 166 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 167 | k8s.io/api v0.0.0-20181121071145-b7bd5f2d334c h1:aSW17ws1n3Y/gxcAggEFSs+UJlzpE3+stTPLQSiVEno= 168 | k8s.io/api v0.0.0-20181121071145-b7bd5f2d334c/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= 169 | k8s.io/apimachinery v0.0.0-20181126122622-195a1699ff5c h1:UGnr+aaGcGM0DIeV2tn2StnKDL6STeE3DFs1Y/r6jMY= 170 | k8s.io/apimachinery v0.0.0-20181126122622-195a1699ff5c/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= 171 | k8s.io/client-go v9.0.0+incompatible h1:2kqW3X2xQ9SbFvWZjGEHBLlWc1LG9JIJNXWkuqwdZ3A= 172 | k8s.io/client-go v9.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= 173 | k8s.io/klog v0.3.2 h1:qvP/U6CcZ6qyi/qSHlJKdlAboCzo3mT0DAm0XAarpz4= 174 | k8s.io/klog v0.3.2/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 175 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 176 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 177 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2" 11 | core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 12 | "github.com/golang/protobuf/jsonpb" 13 | v1 "k8s.io/api/core/v1" 14 | "sigs.k8s.io/yaml" 15 | ) 16 | 17 | type xDSHandler struct { 18 | controller *Controller 19 | } 20 | 21 | func (h *xDSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 22 | switch req.URL.Path { 23 | case "/v2/discovery:endpoints": 24 | h.handleEDS(w, req) 25 | case "/v2/discovery:listeners": 26 | h.handleLDS(w, req) 27 | case "/v2/discovery:clusters": 28 | h.handleCDS(w, req) 29 | case "/config": 30 | h.handleConfig(w, req) 31 | case "/bootstrap": 32 | h.handleBootstrap(w, req) 33 | case "/validate": 34 | h.handleValidate(w, req) 35 | case "/healthz": 36 | http.Error(w, "ok", 200) 37 | default: 38 | http.Error(w, "not found", 404) 39 | } 40 | } 41 | 42 | func (h *xDSHandler) handleValidate(w http.ResponseWriter, req *http.Request) { 43 | if req.Method != "POST" { 44 | http.Error(w, "method not allowed", 405) 45 | return 46 | } 47 | 48 | var cm v1.ConfigMap 49 | 50 | if body, err := ioutil.ReadAll(req.Body); err != nil { 51 | http.Error(w, err.Error(), 400) 52 | return 53 | } else { 54 | if err := yaml.UnmarshalStrict(body, &cm); err != nil { 55 | http.Error(w, err.Error(), 400) 56 | return 57 | } 58 | 59 | } 60 | 61 | config := NewConfig() 62 | if err := config.Load(&cm); err != nil { 63 | http.Error(w, err.Error(), 400) 64 | return 65 | } 66 | 67 | http.Error(w, "ok", 200) 68 | } 69 | 70 | // Endpoint Discovery Service 71 | func (h *xDSHandler) handleEDS(w http.ResponseWriter, req *http.Request) { 72 | if req.Method != "POST" { 73 | http.Error(w, "method not allowed", 405) 74 | return 75 | } 76 | 77 | dr, err := readDiscoveryRequest(req) 78 | if err != nil { 79 | log.Println(err.Error()) 80 | http.Error(w, err.Error(), 500) 81 | return 82 | } 83 | 84 | if len(dr.ResourceNames) != 1 { 85 | http.Error(w, "must have 1 resource_names", 400) 86 | return 87 | } 88 | 89 | if ep, ok := h.controller.GetEndpoints(dr.ResourceNames[0]); ok { 90 | if ep.version == dr.VersionInfo { 91 | w.WriteHeader(304) 92 | return 93 | } 94 | w.Write(ep.data) 95 | } else { 96 | http.Error(w, "not found", 404) 97 | } 98 | } 99 | 100 | // Listener Discovery Service 101 | func (h *xDSHandler) handleLDS(w http.ResponseWriter, req *http.Request) { 102 | if req.Method != "POST" { 103 | http.Error(w, "method not allowed", 405) 104 | return 105 | } 106 | 107 | dr, err := readDiscoveryRequest(req) 108 | if err != nil { 109 | log.Println(err.Error()) 110 | http.Error(w, err.Error(), 500) 111 | return 112 | } 113 | 114 | c := h.controller.GetConfigSnapshot() 115 | if c.version == dr.VersionInfo { 116 | w.WriteHeader(304) 117 | return 118 | } 119 | 120 | if b, ok := c.GetListeners(dr.Node); ok { 121 | w.Write(b) 122 | } else { 123 | http.Error(w, "not found", 404) 124 | } 125 | } 126 | 127 | // Cluster Discovery Service 128 | func (h *xDSHandler) handleCDS(w http.ResponseWriter, req *http.Request) { 129 | if req.Method != "POST" { 130 | http.Error(w, "method not allowed", 405) 131 | return 132 | } 133 | 134 | dr, err := readDiscoveryRequest(req) 135 | if err != nil { 136 | log.Println(err.Error()) 137 | http.Error(w, err.Error(), 500) 138 | return 139 | } 140 | 141 | c := h.controller.GetConfigSnapshot() 142 | if c.version == dr.VersionInfo { 143 | w.WriteHeader(304) 144 | return 145 | } 146 | 147 | if b, ok := c.GetClusters(dr.Node); ok { 148 | w.Write(b) 149 | } else { 150 | http.Error(w, "not found", 404) 151 | } 152 | } 153 | 154 | func (h *xDSHandler) handleConfig(w http.ResponseWriter, req *http.Request) { 155 | if req.Method != "GET" { 156 | http.Error(w, "method not allowed", 405) 157 | return 158 | } 159 | 160 | status := 200 161 | lastError := "" 162 | lastUpdate := h.controller.configStore.lastUpdate 163 | 164 | if h.controller.configStore.lastError != nil { 165 | status = 500 166 | lastError = h.controller.configStore.lastError.Error() 167 | } 168 | 169 | j, _ := json.Marshal(struct { 170 | Version string `json:"version"` 171 | LastError string `json:"last_error"` 172 | LastUpdate time.Time `json:"last_update"` 173 | }{ 174 | h.controller.configStore.config.version, 175 | lastError, 176 | lastUpdate, 177 | }) 178 | 179 | w.WriteHeader(status) 180 | w.Write(j) 181 | } 182 | 183 | func (h *xDSHandler) handleBootstrap(w http.ResponseWriter, req *http.Request) { 184 | if req.Method != "GET" { 185 | http.Error(w, "method not allowed", 405) 186 | return 187 | } 188 | 189 | clusterValues, ok := req.URL.Query()["cluster"] 190 | if !ok || len(clusterValues) != 1 { 191 | http.Error(w, "invalid cluster parameter", 400) 192 | return 193 | } 194 | 195 | idValues, ok := req.URL.Query()["id"] 196 | if !ok || len(idValues) != 1 { 197 | http.Error(w, "invalid id parameter", 400) 198 | return 199 | } 200 | 201 | node := &core.Node{ 202 | Id: idValues[0], 203 | Cluster: clusterValues[0], 204 | } 205 | 206 | configSnapshot := h.controller.configStore.GetConfigSnapshot() 207 | 208 | endpointData := make(map[string][]byte) 209 | 210 | clusterData, ok := configSnapshot.GetClusters(node) 211 | if ok { 212 | clusterNames := configSnapshot.GetClusterNames(node) 213 | for _, clusterName := range clusterNames { 214 | cluster, ok := configSnapshot.clusters[clusterName] 215 | if !ok { 216 | log.Printf("warning: failed to dump bootstrap data for cluster %v", clusterName) 217 | continue 218 | } 219 | 220 | // We only attempt to pre-export endpoints for EDS clusters 221 | if cluster.GetType() != v2.Cluster_EDS { 222 | continue 223 | } 224 | 225 | if endpoint, ok := h.controller.epStore.Get(cluster.EdsClusterConfig.ServiceName); ok { 226 | endpointData[cluster.EdsClusterConfig.ServiceName] = endpoint.data 227 | } 228 | } 229 | } 230 | 231 | listenerData, _ := configSnapshot.GetListeners(node) 232 | 233 | data, err := json.Marshal(bootstrapData{ 234 | Clusters: clusterData, 235 | Listeners: listenerData, 236 | Endpoints: endpointData, 237 | }) 238 | if err != nil { 239 | http.Error(w, "encoding error", 500) 240 | } else { 241 | w.Write(data) 242 | } 243 | } 244 | 245 | func readDiscoveryRequest(req *http.Request) (*v2.DiscoveryRequest, error) { 246 | var dr v2.DiscoveryRequest 247 | err := (&jsonpb.Unmarshaler{ 248 | AllowUnknownFields: true, 249 | }).Unmarshal(req.Body, &dr) 250 | return &dr, err 251 | } 252 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestValidateHandler405(t *testing.T) { 11 | req, err := http.NewRequest("GET", "/validate", nil) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | rr := httptest.NewRecorder() 16 | handler := http.HandlerFunc((&xDSHandler{}).handleValidate) 17 | handler.ServeHTTP(rr, req) 18 | 19 | if rr.Code != http.StatusMethodNotAllowed { 20 | t.Fatal("GET method should respond with 405 not allowed.") 21 | } 22 | } 23 | 24 | func TestValidateHandler400(t *testing.T) { 25 | req, err := http.NewRequest( 26 | "POST", 27 | "/validate", 28 | strings.NewReader("invalid yaml"), 29 | ) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | rr := httptest.NewRecorder() 34 | handler := http.HandlerFunc((&xDSHandler{}).handleValidate) 35 | handler.ServeHTTP(rr, req) 36 | 37 | if rr.Code != http.StatusBadRequest { 38 | t.Fatal("Invalid yaml should respond with 400.") 39 | } 40 | } 41 | 42 | func TestValidateHandler400InvalidConfigmap(t *testing.T) { 43 | req, err := http.NewRequest( 44 | "POST", 45 | "/validate", 46 | strings.NewReader("data: {\"listeners\": 1}"), 47 | ) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | rr := httptest.NewRecorder() 52 | handler := http.HandlerFunc((&xDSHandler{}).handleValidate) 53 | handler.ServeHTTP(rr, req) 54 | 55 | if rr.Code != http.StatusBadRequest { 56 | t.Fatal("Invalid configmap should respond with 400.") 57 | } 58 | } 59 | 60 | func TestValidateHandler200(t *testing.T) { 61 | req, err := http.NewRequest( 62 | "POST", 63 | "/validate", 64 | strings.NewReader(` 65 | --- 66 | apiVersion: v1 67 | kind: ConfigMap 68 | metadata: 69 | name: xds 70 | data: 71 | listeners: | 72 | - name: foo 73 | address: 74 | socket_address: 75 | address: 0.0.0.0 76 | port_value: 10001 77 | clusters: | 78 | - name: foo 79 | type: EDS 80 | connect_timeout: 0.25s 81 | eds_cluster_config: 82 | service_name: default/foo 83 | eds_config: 84 | api_config_source: 85 | api_type: REST 86 | cluster_names: [xds_cluster] 87 | refresh_delay: 1s 88 | assignemtns: | 89 | by-cluster: 90 | foo: 91 | listeners: 92 | - foo 93 | clusters: 94 | - foo 95 | by-node-id: 96 | foo: 97 | listeners: 98 | - foo 99 | clusters: 100 | - foo 101 | `), 102 | ) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | rr := httptest.NewRecorder() 107 | handler := http.HandlerFunc((&xDSHandler{}).handleValidate) 108 | handler.ServeHTTP(rr, req) 109 | 110 | // TODO(michal): Stricter validation for missing data.{listeners,clusters,assignemtns} 111 | // resp. define schema for the data field of configmap 112 | if rr.Code != http.StatusOK { 113 | t.Fatal("Even an empty body is valid configmap.") 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | 15 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/health_check/v3" 16 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 17 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/ratelimit/v3" 18 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/redis_proxy/v3" 19 | _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" 20 | 21 | _ "github.com/envoyproxy/go-control-plane/envoy/config/filter/http/health_check/v2" 22 | _ "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2" 23 | _ "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/rate_limit/v2" 24 | _ "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/redis_proxy/v2" 25 | _ "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/tcp_proxy/v2" 26 | "github.com/hashicorp/go-multierror" 27 | "github.com/mitchellh/go-homedir" 28 | "sigs.k8s.io/yaml" 29 | 30 | v1 "k8s.io/api/core/v1" 31 | "k8s.io/client-go/kubernetes" 32 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 33 | "k8s.io/client-go/rest" 34 | "k8s.io/client-go/tools/clientcmd" 35 | "k8s.io/klog" 36 | ) 37 | 38 | var ( 39 | mode = flag.String("mode", "server", "what mode to run xds in (server / proxy)") 40 | upstreamProxy = flag.String("upstream-proxy", "", "upstream proxy (if running in proxy mode)") 41 | configName = flag.String("config-name", "", "configmap name to use for xds configuration (if running in server mode)") 42 | bootstrapDataDir = flag.String("bootstrap-data", "", "bootstrap data directory (if running in proxy mode)") 43 | serviceNode = flag.String("service-node", "", "service node name") 44 | serviceCluster = flag.String("service-cluster", "", "service cluster name") 45 | concurrency = flag.Int("concurrency", 1, "envoy concurrency") 46 | listen = flag.String("listen", "", "listen address for web service") 47 | validate = flag.String("validate", "", "Path to config map to validate. `-` reads from stdin.") 48 | ) 49 | 50 | // ReadFileorStdin returns content of file or stdin. 51 | func ReadFileorStdin(filePath string) ([]byte, error) { 52 | var fileHandler *os.File 53 | 54 | if filePath == "-" { 55 | fileHandler = os.Stdin 56 | } else { 57 | var err error 58 | fileHandler, err = os.Open(filePath) 59 | defer fileHandler.Close() 60 | if err != nil { 61 | return nil, err 62 | } 63 | } 64 | 65 | buf := bytes.NewBuffer(nil) 66 | io.Copy(buf, fileHandler) 67 | 68 | return buf.Bytes(), nil 69 | } 70 | 71 | // K8SConfig returns a *restclient.Config for initializing a K8S client. 72 | // This configuration first attempts to load a local kubeconfig if a 73 | // path is given. If that doesn't work, then in-cluster auth is used. 74 | func K8SConfig() (*rest.Config, error) { 75 | dir, err := homedir.Dir() 76 | if err != nil { 77 | return nil, fmt.Errorf("error retrieving home directory: %s", err) 78 | } 79 | kubeconfig := filepath.Join(dir, ".kube", "config") 80 | 81 | // First try to get the configuration from the kubeconfig value 82 | config, configErr := clientcmd.BuildConfigFromFlags("", kubeconfig) 83 | if configErr != nil { 84 | configErr = fmt.Errorf("error loading kubeconfig: %s", configErr) 85 | 86 | // kubeconfig failed, fall back and try in-cluster config. We do 87 | // this as the fallback since this makes network connections and 88 | // is much slower to fail. 89 | var err error 90 | config, err = rest.InClusterConfig() 91 | if err != nil { 92 | return nil, multierror.Append(configErr, fmt.Errorf( 93 | "error loading in-cluster config: %s", err)) 94 | } 95 | } 96 | 97 | return config, nil 98 | } 99 | 100 | func runServerMode() { 101 | config, err := K8SConfig() 102 | if err != nil { 103 | log.Println(err) 104 | klog.Fatal(err) 105 | } 106 | 107 | flag.Parse() 108 | 109 | client, err := kubernetes.NewForConfig(config) 110 | if err != nil { 111 | log.Println(err) 112 | klog.Fatal(err) 113 | } 114 | 115 | if *configName == "" { 116 | *configName = os.Getenv("XDS_CONFIGMAP") 117 | if *configName == "" { 118 | log.Fatalf("Must pass -config-name argument or XDS_CONFIGMAP environment variable") 119 | } 120 | } 121 | 122 | // synchronously fetches initial state and sets things up 123 | c := NewController(client, *configName) 124 | c.Run() 125 | serveHTTP(&xDSHandler{c}) 126 | } 127 | 128 | func runProxyMode() { 129 | bootstrapData, err := readBootstrapData(path.Join(*bootstrapDataDir, "bootstrap.json")) 130 | if err != nil { 131 | panic(err) 132 | } 133 | 134 | proxy, err := newProxy(*upstreamProxy, bootstrapData) 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | err = runEnvoy(*serviceNode, *serviceCluster, *upstreamProxy, path.Join(*bootstrapDataDir, "envoy.yaml"), *concurrency) 140 | if err != nil { 141 | panic(err) 142 | } 143 | 144 | serveHTTP(proxy) 145 | } 146 | 147 | func runBootstrapMode() { 148 | values := url.Values{ 149 | "cluster": []string{*serviceCluster}, 150 | "id": []string{*serviceNode}, 151 | } 152 | 153 | bootstrapURL := fmt.Sprintf("http://%s/bootstrap?%s", *upstreamProxy, values.Encode()) 154 | log.Printf("Downloading bootstrap file from %s", bootstrapURL) 155 | 156 | req, err := http.NewRequest("GET", bootstrapURL, nil) 157 | if err != nil { 158 | panic(err) 159 | } 160 | 161 | client := &http.Client{} 162 | resp, err := client.Do(req) 163 | if err != nil { 164 | panic(err) 165 | } 166 | defer resp.Body.Close() 167 | 168 | bootstrapFilePath := path.Join(*bootstrapDataDir, "bootstrap.json") 169 | out, err := os.Create(bootstrapFilePath) 170 | if err != nil { 171 | panic(err) 172 | } 173 | defer out.Close() 174 | 175 | _, err = io.Copy(out, resp.Body) 176 | if err != nil { 177 | panic(err) 178 | } 179 | log.Printf("Finished downloading bootstrap file to %s", bootstrapFilePath) 180 | } 181 | 182 | func serveHTTP(handler http.Handler) { 183 | log.Println("ready.") 184 | 185 | if *listen == "" { 186 | *listen = os.Getenv("XDS_LISTEN") 187 | if *listen == "" { 188 | log.Fatalf("Must pass -listen argument or XDS_LISTEN environment variable") 189 | } 190 | } 191 | 192 | http.ListenAndServe(*listen, handler) 193 | } 194 | 195 | func validateConfig(configPath string) { 196 | log.Printf("Validating: %s\n", configPath) 197 | cmRaw, err := ReadFileorStdin(configPath) 198 | if err != nil { 199 | log.Fatal(err) 200 | } 201 | 202 | var cm v1.ConfigMap 203 | 204 | err = yaml.UnmarshalStrict(cmRaw, &cm) 205 | if err != nil { 206 | log.Fatal(err) 207 | } 208 | 209 | config := NewConfig() 210 | if err := config.Load(&cm); err != nil { 211 | log.Fatal(err) 212 | } 213 | log.Println("Configuration is valid.") 214 | } 215 | 216 | func main() { 217 | flag.Parse() 218 | log.SetFlags(log.LstdFlags | log.Lshortfile) 219 | 220 | if *validate != "" { 221 | validateConfig(*validate) 222 | return 223 | } 224 | 225 | if *mode == "proxy" || *mode == "bootstrap" { 226 | if *upstreamProxy == "" { 227 | log.Fatalf("Must pass 'upstream-proxy'") 228 | } 229 | if *bootstrapDataDir == "" { 230 | log.Fatalf("Must pass 'bootstrap-data-dir'") 231 | } 232 | if *serviceNode == "" { 233 | log.Fatalf("Must pass 'service-node'") 234 | } 235 | if *serviceCluster == "" { 236 | log.Fatalf("Must pass 'service-cluster'") 237 | } 238 | } 239 | 240 | switch *mode { 241 | case "server": 242 | runServerMode() 243 | case "proxy": 244 | runProxyMode() 245 | case "bootstrap": 246 | runBootstrapMode() 247 | default: 248 | log.Fatalf("Invalid XDS mode (only 'server' and 'proxy' are supported): %s", *mode) 249 | } 250 | 251 | } 252 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/http/httputil" 7 | "net/url" 8 | ) 9 | 10 | type proxy struct { 11 | bootstrapData *bootstrapData 12 | reverseProxy *httputil.ReverseProxy 13 | readClusters bool 14 | readListeners bool 15 | readEndpoints bool 16 | } 17 | 18 | func newProxy(upstreamURLRaw string, bootstrapData *bootstrapData) (*proxy, error) { 19 | log.Printf("Running in proxy mode (upstream is '%s')", upstreamURLRaw) 20 | upstreamURL, err := url.Parse(upstreamURLRaw) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | reverseProxy := httputil.NewSingleHostReverseProxy(upstreamURL) 26 | return &proxy{ 27 | bootstrapData: bootstrapData, 28 | reverseProxy: reverseProxy, 29 | }, nil 30 | } 31 | 32 | func (p *proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { 33 | switch req.URL.Path { 34 | case "/v2/discovery:endpoints": 35 | p.handleEDS(w, req) 36 | case "/v2/discovery:listeners": 37 | p.handleLDS(w, req) 38 | case "/v2/discovery:clusters": 39 | p.handleCDS(w, req) 40 | case "/healthz": 41 | if p.readEndpoints { 42 | http.Error(w, "ok", 200) 43 | } else { 44 | http.Error(w, "bootstrapping", 500) 45 | } 46 | default: 47 | http.Error(w, "not found", 404) 48 | } 49 | } 50 | 51 | func (p *proxy) handleEDS(w http.ResponseWriter, req *http.Request) { 52 | if p.readEndpoints { 53 | p.reverseProxy.ServeHTTP(w, req) 54 | return 55 | } 56 | 57 | if req.Method != "POST" { 58 | http.Error(w, "method not allowed", 405) 59 | return 60 | } 61 | 62 | dr, err := readDiscoveryRequest(req) 63 | if err != nil { 64 | http.Error(w, err.Error(), 500) 65 | return 66 | } 67 | 68 | if len(dr.ResourceNames) != 1 { 69 | http.Error(w, "must have 1 resource_names", 400) 70 | return 71 | } 72 | 73 | p.bootstrapData.endpointsLock.Lock() 74 | defer p.bootstrapData.endpointsLock.Unlock() 75 | if _, exists := p.bootstrapData.Endpoints[dr.ResourceNames[0]]; exists { 76 | log.Printf("serving request for '%s' endpoints from bootstrap data", dr.ResourceNames[0]) 77 | w.Write(p.bootstrapData.Endpoints[dr.ResourceNames[0]]) 78 | delete(p.bootstrapData.Endpoints, dr.ResourceNames[0]) 79 | if len(p.bootstrapData.Endpoints) == 0 { 80 | p.readEndpoints = true 81 | } 82 | } else { 83 | log.Printf("eds failed, endpoints for that cluster already read") 84 | http.Error(w, "unavailable bootstrap data", 500) 85 | } 86 | } 87 | 88 | func (p *proxy) handleLDS(w http.ResponseWriter, req *http.Request) { 89 | if p.readListeners { 90 | p.reverseProxy.ServeHTTP(w, req) 91 | return 92 | } 93 | 94 | if req.Method != "POST" { 95 | http.Error(w, "method not allowed", 405) 96 | return 97 | } 98 | 99 | _, err := readDiscoveryRequest(req) 100 | if err != nil { 101 | log.Println(err.Error()) 102 | http.Error(w, err.Error(), 500) 103 | return 104 | } 105 | 106 | log.Printf("serving listeners request from bootstrap data") 107 | w.Write(p.bootstrapData.Listeners) 108 | } 109 | 110 | func (p *proxy) handleCDS(w http.ResponseWriter, req *http.Request) { 111 | if p.readClusters { 112 | p.reverseProxy.ServeHTTP(w, req) 113 | return 114 | } 115 | 116 | if req.Method != "POST" { 117 | http.Error(w, "method not allowed", 405) 118 | return 119 | } 120 | 121 | _, err := readDiscoveryRequest(req) 122 | if err != nil { 123 | log.Println(err.Error()) 124 | http.Error(w, err.Error(), 500) 125 | return 126 | } 127 | 128 | log.Printf("serving clusters request from boostrap data") 129 | w.Write(p.bootstrapData.Clusters) 130 | } 131 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "strings" 7 | 8 | "github.com/golang/protobuf/jsonpb" 9 | "github.com/golang/protobuf/proto" 10 | "sigs.k8s.io/yaml" 11 | ) 12 | 13 | // Convert a YAML list into a slice of interfaces 14 | func unmarshalYAMLSlice(b []byte) ([]interface{}, error) { 15 | var raw []interface{} 16 | err := yaml.Unmarshal(b, &raw) 17 | return raw, err 18 | } 19 | 20 | // To convert into a valid proto.Message, it needs to first 21 | // be encoded into JSON, then loaded through jsonpb 22 | func convertToPb(data interface{}, pb proto.Message) error { 23 | j, err := json.Marshal(data) 24 | if err != nil { 25 | return err 26 | } 27 | return jsonpb.Unmarshal(bytes.NewReader(j), pb) 28 | } 29 | 30 | func structToJSON(pb proto.Message) ([]byte, error) { 31 | var b bytes.Buffer 32 | if err := (&jsonpb.Marshaler{OrigName: true}).Marshal(&b, pb); err != nil { 33 | return nil, err 34 | } 35 | return b.Bytes(), nil 36 | } 37 | 38 | func structToYAML(pb proto.Message) ([]byte, error) { 39 | j, err := structToJSON(pb) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return yaml.JSONToYAML(j) 44 | } 45 | 46 | func k8sSplitName(name string) (string, string) { 47 | s := strings.SplitN(name, "/", 2) 48 | return s[0], s[1] 49 | } 50 | --------------------------------------------------------------------------------