├── .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 |
--------------------------------------------------------------------------------