├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── Dockerfile-xds-build ├── Dockerfile-xds-server ├── Gopkg.lock ├── Gopkg.toml ├── Jenkinsfile ├── LICENSE ├── README.md ├── cmd └── server │ ├── cache │ └── cache.go │ ├── constant │ └── const.go │ ├── dump │ └── config_dump.go │ ├── main.go │ ├── manager │ └── manager.go │ ├── mapper │ ├── clusterMapper.go │ ├── clusterMapper_test.go │ ├── common.go │ ├── endpointMapper.go │ ├── listenerMapper.go │ ├── listenerMapper_test.go │ ├── mapper.go │ └── routeMapper.go │ ├── metrics │ └── metrics.go │ ├── model │ ├── config_meta.go │ └── envoy_subscriber.go │ ├── server │ ├── adsImpl.go │ ├── clusterImpl.go │ ├── endpointImpl.go │ ├── listenerImpl.go │ ├── routeImpl.go │ ├── server.go │ └── server_test.go │ ├── service │ ├── dispatchService.go │ ├── registerService.go │ ├── v2HelperService.go │ └── watchService.go │ ├── storage │ ├── consul.go │ ├── consulConfigDao.go │ ├── fileConfigDao.go │ ├── subscriberDao.go │ └── xdsConfigDao.go │ └── util │ └── util.go ├── docker-compose.consul.yaml ├── docker-compose.server.yml ├── docker-compose.travis.yaml ├── env_values.txt ├── lib └── api.pb.go ├── snippet └── test ├── envoy ├── config.yaml ├── config_ads.yaml └── config_lds.yaml ├── integration ├── .env ├── cert.pem ├── config-ads.yaml ├── config-cds-lds.yaml ├── docker-compose.yaml ├── dummy-app │ ├── Dockerfile-dummy-app │ └── main.go └── key.pem └── rspec ├── basic_spec.rb └── json ├── cluster_0.json ├── cluster_1.json ├── cluster_2.json ├── cluster_3.json ├── listener_0.json ├── listener_1.json ├── listener_2.json └── listener_3.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: ruby 4 | 5 | services: 6 | - docker 7 | 8 | rvm: 9 | - 2.5 10 | 11 | before_script: 12 | - docker network create envoy-pilot_xds-demo 13 | - docker-compose -f docker-compose.consul.yaml up -d 14 | - sleep 30 15 | - docker-compose -f docker-compose.travis.yaml up --build -d 16 | - (cd test/integration/ && docker-compose up --build -d) 17 | - gem install rspec 18 | - gem install rest-client 19 | - gem install diplomat 20 | - gem install plissken 21 | 22 | script: 23 | - (cd test/rspec && rspec basic_spec.rb --order defined --format documentation) 24 | 25 | after_script: 26 | - docker-compose -f docker-compose.consul.yaml down 27 | - docker-compose -f docker-compose.travis.yaml down 28 | - (cd test/integration/ && docker-compose down) 29 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "Remote server", 10 | "type": "go", 11 | "request": "launch", 12 | "mode": "remote", 13 | "remotePath": "/go/src/Envoy-Pilot/cmd/server/", 14 | "port": 2345, 15 | "host": "127.0.0.1", 16 | "program": "${workspaceRoot}/cmd/server/", 17 | "env": {}, 18 | "args": [] 19 | }, 20 | { 21 | "name": "Launch", 22 | "type": "go", 23 | "request": "launch", 24 | "mode": "debug", 25 | "remotePath": "", 26 | "port": 2345, 27 | "host": "127.0.0.1", 28 | "program": "${fileDirname}", 29 | "env": {}, 30 | "args": [], 31 | "showLog": true 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /Dockerfile-xds-build: -------------------------------------------------------------------------------- 1 | # FROM golang:latest 2 | 3 | # RUN go get -u github.com/lyft/protoc-gen-validate 4 | # RUN go get -u github.com/golang/protobuf/proto 5 | # RUN go get -u github.com/golang/protobuf/protoc-gen-go 6 | # RUN go get -u golang.org/x/net/context 7 | # RUN go get -u google.golang.org/grpc 8 | 9 | # RUN mkdir -p /go/src/github.com/envoyproxy 10 | # WORKDIR /go/src/github.com/envoyproxy/ 11 | # RUN git clone --branch v0.4 https://github.com/envoyproxy/go-control-plane.git 12 | 13 | # RUN mkdir -p $GOPATH/src/github.com/gogo 14 | # WORKDIR $GOPATH/src/github.com/gogo 15 | # RUN git clone https://github.com/gogo/googleapis.git 16 | # RUN git clone https://github.com/gogo/protobuf.git 17 | # RUN go get github.com/derekparker/delve/cmd/dlv 18 | # RUN go get github.com/google/uuid 19 | # RUN go get github.com/hashicorp/consul/api 20 | # RUN go get github.com/joho/godotenv 21 | 22 | # RUN mkdir -p $GOPATH/src/github.com/prometheus 23 | # WORKDIR $GOPATH/src/github.com/prometheus 24 | # RUN git clone https://github.com/prometheus/client_golang.git 25 | # RUN git clone https://github.com/prometheus/common.git 26 | # RUN git clone https://github.com/prometheus/client_model.git 27 | # RUN git clone https://github.com/prometheus/procfs.git 28 | 29 | # RUN mkdir -p $GOPATH/src/github.com/beorn7/ 30 | # WORKDIR $GOPATH/src/github.com/beorn7/ 31 | # RUN git clone https://github.com/beorn7/perks.git 32 | 33 | # RUN mkdir -p $GOPATH/src/github.com/matttproud 34 | # WORKDIR $GOPATH/src/github.com/matttproud 35 | # RUN git clone https://github.com/matttproud/golang_protobuf_extensions.git 36 | 37 | # RUN go get github.com/rs/xid 38 | 39 | # # TODO Compile on build 40 | # #RUN mkdir res 41 | # #RUN find $GOPATH/src/envoy -name '*.proto' | xargs -I % sh -c 'protoc --go_out=/res/ % --proto_path=$GOPATH/src/ \ 42 | # # --proto_path=$GOPATH/src/github.com/lyft/protoc-gen-validate \ 43 | # # --proto_path=/gogo-genproto/prometheus \ 44 | # # --proto_path=/gogo-genproto/googleapis/ \ 45 | # # --proto_path=/gogo-genproto/opencensus/proto \ 46 | # # --proto_path=/gogo-genproto/opencensus/proto/trace' 47 | # # 48 | 49 | FROM golang:latest 50 | 51 | RUN go get -u github.com/golang/dep/cmd/dep 52 | RUN go get -u github.com/derekparker/delve/cmd/dlv 53 | 54 | RUN mkdir /go/src/Envoy-Pilot 55 | ADD Gopkg.lock /go/src/Envoy-Pilot/ 56 | ADD Gopkg.toml /go/src/Envoy-Pilot/ 57 | 58 | WORKDIR /go/src/Envoy-Pilot/ 59 | 60 | RUN ls -l 61 | RUN dep ensure -vendor-only -------------------------------------------------------------------------------- /Dockerfile-xds-server: -------------------------------------------------------------------------------- 1 | FROM tak2siva/xds-build:v0.1.6 as builder 2 | 3 | RUN go get -u github.com/derekparker/delve/cmd/dlv 4 | ADD cmd /go/src/Envoy-Pilot/cmd 5 | 6 | RUN rm -rf /go/src/github.com/envoyproxy/go-control-plane/vendor 7 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /go/bin/Envoy-Pilot /go/src/Envoy-Pilot/cmd/server/main.go 8 | 9 | FROM alpine:latest 10 | RUN apk --no-cache add ca-certificates 11 | RUN mkdir -p /go/bin/ 12 | WORKDIR /go/bin/ 13 | COPY --from=builder /go/bin/Envoy-Pilot . 14 | CMD ["./Envoy-Pilot"] -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/armon/go-metrics" 6 | packages = ["."] 7 | revision = "3df31a1ada83e310c2e24b267c8e8b68836547b4" 8 | 9 | [[projects]] 10 | branch = "master" 11 | name = "github.com/beorn7/perks" 12 | packages = ["quantile"] 13 | revision = "3a771d992973f24aa725d07868b467d1ddfceafb" 14 | 15 | [[projects]] 16 | name = "github.com/envoyproxy/go-control-plane" 17 | packages = [ 18 | "envoy/api/v2", 19 | "envoy/api/v2/auth", 20 | "envoy/api/v2/cluster", 21 | "envoy/api/v2/core", 22 | "envoy/api/v2/endpoint", 23 | "envoy/api/v2/listener", 24 | "envoy/api/v2/route", 25 | "envoy/config/accesslog/v2", 26 | "envoy/config/filter/accesslog/v2", 27 | "envoy/config/filter/http/health_check/v2", 28 | "envoy/config/filter/network/http_connection_manager/v2", 29 | "envoy/service/discovery/v2", 30 | "envoy/type", 31 | "pkg/cache", 32 | "pkg/log", 33 | "pkg/util" 34 | ] 35 | revision = "c14704578b82128fab17743d91f870c406ef8d09" 36 | version = "v0.4" 37 | 38 | [[projects]] 39 | name = "github.com/ghodss/yaml" 40 | packages = ["."] 41 | revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" 42 | version = "v1.0.0" 43 | 44 | [[projects]] 45 | name = "github.com/go-test/deep" 46 | packages = ["."] 47 | revision = "6592d9cc0a499ad2d5f574fde80a2b5c5cc3b4f5" 48 | version = "v1.0.1" 49 | 50 | [[projects]] 51 | name = "github.com/gogo/googleapis" 52 | packages = [ 53 | "google/api", 54 | "google/rpc" 55 | ] 56 | revision = "0cd9801be74a10d5ac39d69626eac8255ffcd502" 57 | 58 | [[projects]] 59 | name = "github.com/gogo/protobuf" 60 | packages = [ 61 | "gogoproto", 62 | "jsonpb", 63 | "proto", 64 | "protoc-gen-gogo/descriptor", 65 | "sortkeys", 66 | "types" 67 | ] 68 | revision = "636bf0302bc95575d69441b25a2603156ffdddf1" 69 | version = "v1.1.1" 70 | 71 | [[projects]] 72 | name = "github.com/golang/protobuf" 73 | packages = [ 74 | "proto", 75 | "protoc-gen-go/descriptor", 76 | "ptypes", 77 | "ptypes/any", 78 | "ptypes/duration", 79 | "ptypes/timestamp" 80 | ] 81 | revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" 82 | version = "v1.2.0" 83 | 84 | [[projects]] 85 | name = "github.com/google/uuid" 86 | packages = ["."] 87 | revision = "d460ce9f8df2e77fb1ba55ca87fafed96c607494" 88 | version = "v1.0.0" 89 | 90 | [[projects]] 91 | name = "github.com/hashicorp/consul" 92 | packages = ["api"] 93 | revision = "e8757838a49feeb682c7e6ad6b78694a78b2096b" 94 | version = "v1.3.0" 95 | 96 | [[projects]] 97 | name = "github.com/hashicorp/go-cleanhttp" 98 | packages = ["."] 99 | revision = "d5fe4b57a186c716b0e00b8c301cbd9b4182694d" 100 | 101 | [[projects]] 102 | branch = "master" 103 | name = "github.com/hashicorp/go-rootcerts" 104 | packages = ["."] 105 | revision = "6bb64b370b90e7ef1fa532be9e591a81c3493e00" 106 | 107 | [[projects]] 108 | name = "github.com/hashicorp/serf" 109 | packages = ["coordinate"] 110 | revision = "19bbd39e421bdf3559d5025fb2c760f5ffa56233" 111 | 112 | [[projects]] 113 | name = "github.com/joho/godotenv" 114 | packages = ["."] 115 | revision = "23d116af351c84513e1946b527c88823e476be13" 116 | version = "v1.3.0" 117 | 118 | [[projects]] 119 | name = "github.com/lyft/protoc-gen-validate" 120 | packages = ["validate"] 121 | revision = "f9d2b11e44149635b23a002693b76512b01ae515" 122 | 123 | [[projects]] 124 | name = "github.com/matttproud/golang_protobuf_extensions" 125 | packages = ["pbutil"] 126 | revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" 127 | version = "v1.0.1" 128 | 129 | [[projects]] 130 | name = "github.com/mitchellh/go-homedir" 131 | packages = ["."] 132 | revision = "ae18d6b8b3205b561c79e8e5f69bff09736185f4" 133 | version = "v1.0.0" 134 | 135 | [[projects]] 136 | name = "github.com/mitchellh/mapstructure" 137 | packages = ["."] 138 | revision = "5a380f224700b8a6c4eaad048804f5bff514cb35" 139 | 140 | [[projects]] 141 | name = "github.com/prometheus/client_golang" 142 | packages = [ 143 | "prometheus", 144 | "prometheus/promhttp" 145 | ] 146 | revision = "c5b7fccd204277076155f10851dad72b76a49317" 147 | version = "v0.8.0" 148 | 149 | [[projects]] 150 | branch = "master" 151 | name = "github.com/prometheus/client_model" 152 | packages = ["go"] 153 | revision = "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f" 154 | 155 | [[projects]] 156 | branch = "master" 157 | name = "github.com/prometheus/common" 158 | packages = [ 159 | "expfmt", 160 | "internal/bitbucket.org/ww/goautoneg", 161 | "model" 162 | ] 163 | revision = "c7de2306084e37d54b8be01f3541a8464345e9a5" 164 | 165 | [[projects]] 166 | branch = "master" 167 | name = "github.com/prometheus/procfs" 168 | packages = [ 169 | ".", 170 | "internal/util", 171 | "nfs", 172 | "xfs" 173 | ] 174 | revision = "185b4288413d2a0dd0806f78c90dde719829e5ae" 175 | 176 | [[projects]] 177 | name = "github.com/rs/xid" 178 | packages = ["."] 179 | revision = "15d26544def341f036c5f8dca987a4cbe575032c" 180 | version = "v1.2.1" 181 | 182 | [[projects]] 183 | branch = "master" 184 | name = "golang.org/x/net" 185 | packages = [ 186 | "context", 187 | "http/httpguts", 188 | "http2", 189 | "http2/hpack", 190 | "idna", 191 | "internal/timeseries", 192 | "trace" 193 | ] 194 | revision = "49bb7cea24b1df9410e1712aa6433dae904ff66a" 195 | 196 | [[projects]] 197 | branch = "master" 198 | name = "golang.org/x/sys" 199 | packages = ["unix"] 200 | revision = "fa43e7bc11baaae89f3f902b2b4d832b68234844" 201 | 202 | [[projects]] 203 | name = "golang.org/x/text" 204 | packages = [ 205 | "collate", 206 | "collate/build", 207 | "internal/colltab", 208 | "internal/gen", 209 | "internal/tag", 210 | "internal/triegen", 211 | "internal/ucd", 212 | "language", 213 | "secure/bidirule", 214 | "transform", 215 | "unicode/bidi", 216 | "unicode/cldr", 217 | "unicode/norm", 218 | "unicode/rangetable" 219 | ] 220 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 221 | version = "v0.3.0" 222 | 223 | [[projects]] 224 | branch = "master" 225 | name = "google.golang.org/genproto" 226 | packages = ["googleapis/rpc/status"] 227 | revision = "af9cb2a35e7f169ec875002c1829c9b315cddc04" 228 | 229 | [[projects]] 230 | name = "google.golang.org/grpc" 231 | packages = [ 232 | ".", 233 | "balancer", 234 | "balancer/base", 235 | "balancer/roundrobin", 236 | "codes", 237 | "connectivity", 238 | "credentials", 239 | "encoding", 240 | "encoding/proto", 241 | "grpclog", 242 | "internal", 243 | "internal/backoff", 244 | "internal/channelz", 245 | "internal/envconfig", 246 | "internal/grpcrand", 247 | "internal/transport", 248 | "keepalive", 249 | "metadata", 250 | "naming", 251 | "peer", 252 | "reflection", 253 | "reflection/grpc_reflection_v1alpha", 254 | "resolver", 255 | "resolver/dns", 256 | "resolver/passthrough", 257 | "stats", 258 | "status", 259 | "tap" 260 | ] 261 | revision = "8dea3dc473e90c8179e519d91302d0597c0ca1d1" 262 | version = "v1.15.0" 263 | 264 | [[projects]] 265 | name = "gopkg.in/yaml.v2" 266 | packages = ["."] 267 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" 268 | version = "v2.2.1" 269 | 270 | [solve-meta] 271 | analyzer-name = "dep" 272 | analyzer-version = 1 273 | inputs-digest = "45365e9a21ae02b209eed39a928665ae39533824bf2232fa287325dd689cc678" 274 | solver-name = "gps-cdcl" 275 | solver-version = 1 276 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/envoyproxy/go-control-plane" 30 | version = "0.4.0" 31 | 32 | [[constraint]] 33 | name = "github.com/ghodss/yaml" 34 | version = "1.0.0" 35 | 36 | [[constraint]] 37 | name = "github.com/go-test/deep" 38 | version = "1.0.1" 39 | 40 | [[constraint]] 41 | name = "github.com/gogo/protobuf" 42 | version = "1.1.1" 43 | 44 | [[constraint]] 45 | name = "github.com/golang/protobuf" 46 | version = "1.2.0" 47 | 48 | [[constraint]] 49 | name = "github.com/google/uuid" 50 | version = "1.0.0" 51 | 52 | [[constraint]] 53 | name = "github.com/hashicorp/consul" 54 | version = "1.3.0" 55 | 56 | [[constraint]] 57 | name = "github.com/joho/godotenv" 58 | version = "1.3.0" 59 | 60 | [[constraint]] 61 | name = "github.com/prometheus/client_golang" 62 | version = "0.8.0" 63 | 64 | [[constraint]] 65 | name = "github.com/rs/xid" 66 | version = "1.2.1" 67 | 68 | [[constraint]] 69 | branch = "master" 70 | name = "golang.org/x/net" 71 | 72 | [[constraint]] 73 | name = "google.golang.org/grpc" 74 | version = "1.15.0" 75 | 76 | [prune] 77 | go-tests = true 78 | unused-packages = true 79 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | stages { 4 | stage('Step 1') { 5 | steps { 6 | sh 'ls -l' 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sivakumar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Envoy-Pilot 2 | 3 | [![Build Status](https://travis-ci.org/tak2siva/Envoy-Pilot.svg?branch=master)](https://travis-ci.org/tak2siva/Envoy-Pilot) ![Version](https://img.shields.io/badge/version-v0.2.3-yellowgreen.svg) 4 | ![docker pulls](https://img.shields.io/docker/pulls/tak2siva/envoy-pilot) 5 | 6 | 7 | Envoy Pilot or Envoy xDS is a control plane implementation for [Envoy](https://github.com/envoyproxy/envoy) written in Golang and uses Consul for persistence by default. It can also run without Consul by loading configuration from file. 8 | 9 | This is an extension of [go-control-plane](https://github.com/envoyproxy/go-control-plane) 10 | 11 | Currently Supports 12 | * CDS 13 | * LDS 14 | * RDS 15 | * EDS 16 | * ADS (for CDS & LDS) 17 | 18 | *Note: Some infrequent configurations might not be mapped. Feel free to PR* 19 | 20 | Checkout [Envoy XDS PROTOCOL Overview](https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol) for more detail 21 | 22 | Also Checkout Sample Project 23 | * [Envoy xDS Example From File](https://github.com/tak2siva/Envoy-xDS-Example-From-File) 24 | * [Envoy xDS Example With Consul](https://github.com/tak2siva/Envoy-xDS-Example-Consul) 25 | 26 | ## File Config 27 | Checkout the above example to load config from file 28 | 29 | ## Consul Usage 30 | 31 | xDS Server will be exposed on port 7777 32 | 33 | Run Envoy Proxy with the following configurations or use `--service-node` && `--service-cluster` 34 | ``` 35 | node: 36 | id: ride-service-replica-2 37 | cluster: ride-service 38 | ``` 39 | 40 | Every *DS requires two keys to be set in consul 41 | * config 42 | * version 43 | 44 | And the key template is `xDS/app-cluster/ride-service/CDS/(config|version)` 45 | 46 | For CDS add KV pairs 47 | * `xDS/app-cluster/ride-service/CDS/version` => "1.0" 48 | * `xDS/app-cluster/ride-service/CDS/config` => `"[{ 49 | { 50 | "name": "app1", 51 | "connect_timeout": "0.250s", 52 | "type": "STRICT_DNS", 53 | "lb_policy": "RANDOM", 54 | "hosts": [{ 55 | "socket_address": { 56 | "address": "127.0.0.2", 57 | "port_value": 1234 58 | } 59 | }] 60 | } 61 | }]"` 62 | 63 | Pushing new configuration 64 | * Envoy-Pilot will be polling for version change every 10 seconds. 65 | * If there is a version mismatch for any of `xDS/app-cluster/ride-service/(CDS|LDS|RDS|EDS)/version` then new config `xDS/app-cluster/ride-service/(CDS|LDS|RDS|EDS)/config` will be pushed to subscriber envoy. 66 | * If update succeed there will be an ACK log for the instance. 67 | 68 | ## Running Docker Compose 69 | 70 | From root directory 71 | ``` 72 | docker network create envoy-pilot_xds-demo 73 | docker-compose -f docker-compose.consul.yaml up 74 | docker-compose -f docker-compose.server.yaml up --build 75 | ``` 76 | 77 | ## Running test 78 | 79 | ``` 80 | cd test/integration 81 | docker-compose up --build 82 | 83 | cd test/rspec 84 | DEVMODE=true rspec basic_spec.rb --order defined --format documentation 85 | ``` 86 | 87 | 88 | ## Runnnig Docker 89 | 90 | Consul url need to be set in .env 91 | 92 | env_values.txt 93 | ``` 94 | CONSUL_PATH="http://consul-server:8500" 95 | CONSUL_PREFIX=""xDS" 96 | ``` 97 | 98 | Docker run 99 | ``` 100 | docker run -v $(pwd)/env_values.txt:/.env -p 7777:7777 -p 9090:9090 tak2siva/envoy-pilot:latest 101 | ``` 102 | 103 | ## Helm Chart 104 | 105 | Install using the [Helm Chart for Envoy-Pilot](https://github.com/tak2siva/Envoy-Pilot-Helm). 106 | 107 | ## Debugging 108 | 109 | * xDS-Server is running on port 7777 110 | * A http server is running on port 9090 for debugging 111 | 112 | `localhost:9090/dump/KEY_TEMPLATE` will give a json dump of proto mapping 113 | 114 | **Ex:** 115 | ``` 116 | http://localhost:9090/dump/xDS/app-cluster/ride-service/(CDS|LDS|RDS|EDS)/config 117 | ``` 118 | 119 | * To get list of subcsribers hit `localhost:9090/dump/subscribers/` 120 | 121 | 122 | ## Prometheus metrics 123 | 124 | Prometheus is running on `localhost:8081/metrics` and the following stats are available 125 | * xds_active_connections[cluster] (GAUGE) 126 | * xds_active_subscribers[cluster][type] (GAUGE) 127 | * xds_update_counter[cluster][subscribedTo] (Counter) 128 | -------------------------------------------------------------------------------- /cmd/server/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "sync" 4 | 5 | var SUBSCRIBER_CACHE = sync.Map{} 6 | var NONCE_CACHE = sync.Map{} 7 | -------------------------------------------------------------------------------- /cmd/server/constant/const.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | import "time" 4 | 5 | const ( 6 | SUBSCRIBE_CDS = "CDS" 7 | SUBSCRIBE_LDS = "LDS" 8 | SUBSCRIBE_RDS = "RDS" 9 | SUBSCRIBE_ADS = "ADS" 10 | SUBSCRIBE_EDS = "EDS" 11 | ) 12 | 13 | const ENVOY_SUBSCRIBER_KEY = "envoySubscriber" 14 | 15 | // Not really constants 16 | var SUPPORTED_TYPES = []string{SUBSCRIBE_CDS, SUBSCRIBE_LDS, SUBSCRIBE_RDS, SUBSCRIBE_EDS} 17 | var ENV_PATH = "/.env" 18 | var CONSUL_PREFIX = "xDS" 19 | var FILE_MODE = false 20 | var FOLDER_PATH = "" 21 | var POLL_INTERVAL = 10 * time.Second 22 | -------------------------------------------------------------------------------- /cmd/server/dump/config_dump.go: -------------------------------------------------------------------------------- 1 | package dump 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/cache" 5 | "Envoy-Pilot/cmd/server/mapper" 6 | "Envoy-Pilot/cmd/server/model" 7 | "Envoy-Pilot/cmd/server/service" 8 | "Envoy-Pilot/cmd/server/storage" 9 | "Envoy-Pilot/cmd/server/util" 10 | "encoding/json" 11 | "fmt" 12 | "log" 13 | "net/http" 14 | "strings" 15 | ) 16 | 17 | func SetUpHttpServer() { 18 | http.HandleFunc("/ping", pingHandler) 19 | http.HandleFunc("/dump/cds/", configDumpCDS) 20 | http.HandleFunc("/dump/lds/", configDumpLDS) 21 | http.HandleFunc("/dump/subscribers/", subscribersDump) 22 | http.HandleFunc("/dump/topics/", pollTopicsDump) 23 | 24 | log.Println("Starting http server on :9090..") 25 | err := http.ListenAndServe(":9090", nil) 26 | if err != nil { 27 | log.Fatal("ListenAndServe: ", err) 28 | } 29 | } 30 | 31 | func pingHandler(w http.ResponseWriter, r *http.Request) { 32 | fmt.Fprintf(w, "%s", "pong") 33 | } 34 | 35 | func configDumpCDS(w http.ResponseWriter, r *http.Request) { 36 | path := r.URL.Path[1:] 37 | keyPath := strings.Replace(path, "dump/cds/", "", -1) 38 | log.Printf("Getting cds dump for %s\n", keyPath) 39 | m := &mapper.ClusterMapper{} 40 | cwrapper := storage.GetConsulWrapper() 41 | jsonStr := cwrapper.GetString(keyPath) 42 | fmt.Printf("json dump %s\n", jsonStr) 43 | val, err := m.GetClusters(jsonStr) 44 | 45 | if err != nil { 46 | fmt.Fprintf(w, "Error creating obj %s", err) 47 | return 48 | } 49 | resJson, err := json.Marshal(val) 50 | if err != nil { 51 | fmt.Fprintf(w, "Error parsing json %s", err) 52 | return 53 | } 54 | 55 | fmt.Fprintf(w, "%s", resJson) 56 | } 57 | 58 | func configDumpLDS(w http.ResponseWriter, r *http.Request) { 59 | path := r.URL.Path[1:] 60 | keyPath := strings.Replace(path, "dump/lds/", "", -1) 61 | log.Printf("Getting lds dump for %s\n", keyPath) 62 | m := &mapper.ListenerMapper{} 63 | cwrapper := storage.GetConsulWrapper() 64 | jsonStr := cwrapper.GetString(keyPath) 65 | fmt.Printf("json dump %s\n", jsonStr) 66 | val, err := m.GetListeners(jsonStr) 67 | 68 | if err != nil { 69 | fmt.Fprintf(w, "Error creating obj %s", err) 70 | return 71 | } 72 | resJson, err := json.Marshal(val) 73 | if err != nil { 74 | fmt.Fprintf(w, "Error parsing json %s", err) 75 | return 76 | } 77 | 78 | fmt.Fprintf(w, "%s", resJson) 79 | } 80 | 81 | func subscribersDump(w http.ResponseWriter, r *http.Request) { 82 | res := make(map[string]*model.EnvoySubscriber) 83 | cache.SUBSCRIBER_CACHE.Range(func(k interface{}, v interface{}) bool { 84 | res[k.(string)] = v.(*model.EnvoySubscriber) 85 | return true 86 | }) 87 | fmt.Fprintf(w, "%s", util.ToJson(res)) 88 | } 89 | 90 | func pollTopicsDump(w http.ResponseWriter, r *http.Request) { 91 | fmt.Fprintf(w, "%s", util.ToJson(service.GetPollTopics())) 92 | } 93 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "Envoy-Pilot/cmd/server/dump" 6 | "Envoy-Pilot/cmd/server/metrics" 7 | "Envoy-Pilot/cmd/server/server" 8 | "Envoy-Pilot/cmd/server/service" 9 | "Envoy-Pilot/cmd/server/storage" 10 | myUtil "Envoy-Pilot/cmd/server/util" 11 | "fmt" 12 | "log" 13 | "net" 14 | "os" 15 | "time" 16 | 17 | discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2" 18 | consul "github.com/hashicorp/consul/api" 19 | "github.com/joho/godotenv" 20 | 21 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 22 | 23 | "google.golang.org/grpc" 24 | "google.golang.org/grpc/reflection" 25 | ) 26 | 27 | var consulHandle *consul.KV 28 | 29 | func init() { 30 | //host.docker.internal:8500 31 | initEnv() 32 | log.SetFlags(log.LstdFlags | log.Llongfile) 33 | if !constant.FILE_MODE { 34 | consulHealthCheck() 35 | } 36 | server.InitServerDeps() 37 | } 38 | 39 | func consulHealthCheck() { 40 | cwrapper := storage.GetConsulWrapper() 41 | cwrapper.GetUniqId() 42 | } 43 | 44 | func main() { 45 | go dump.SetUpHttpServer() 46 | go metrics.StartMetricsServer() 47 | go service.ConsulPollLoop() 48 | 49 | lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 7777)) 50 | if err != nil { 51 | log.Fatalf("failed to listen: %v", err) 52 | } 53 | 54 | s := grpc.NewServer() 55 | v2.RegisterClusterDiscoveryServiceServer(s, &server.Server{}) 56 | v2.RegisterListenerDiscoveryServiceServer(s, &server.Server{}) 57 | v2.RegisterRouteDiscoveryServiceServer(s, &server.Server{}) 58 | v2.RegisterEndpointDiscoveryServiceServer(s, &server.Server{}) 59 | discovery.RegisterAggregatedDiscoveryServiceServer(s, &server.Server{}) 60 | 61 | reflection.Register(s) 62 | 63 | log.Print("Started grpc server..") 64 | if err := s.Serve(lis); err != nil { 65 | log.Fatalf("failed to serve %v", err) 66 | } 67 | } 68 | 69 | func initEnv() { 70 | err := godotenv.Load(constant.ENV_PATH) 71 | if err != nil { 72 | log.Print(err) 73 | log.Fatal("Error loading .env file") 74 | } 75 | 76 | if len(os.Getenv("POLL_INTERVAL")) > 0 { 77 | res, err := time.ParseDuration(os.Getenv("POLL_INTERVAL")) 78 | myUtil.Check(err) 79 | constant.POLL_INTERVAL = res 80 | } 81 | 82 | if len(os.Getenv("CONSUL_PREFIX")) > 0 { 83 | constant.CONSUL_PREFIX = os.Getenv("CONSUL_PREFIX") 84 | } 85 | 86 | fileMode := os.Getenv("FILE_MODE") 87 | if fileMode == "true" { 88 | constant.FILE_MODE = true 89 | } 90 | 91 | if len(os.Getenv("FOLDER_PATH")) > 0 { 92 | constant.FOLDER_PATH = os.Getenv("FOLDER_PATH") 93 | } 94 | 95 | if constant.FILE_MODE && len(constant.FOLDER_PATH) == 0 { 96 | panic("Missing config folder path env variable FOLDER_PATH..\n") 97 | } 98 | 99 | log.Printf("------- ENV VALUES -----\n") 100 | log.Printf("FILE_MODE: %t", constant.FILE_MODE) 101 | log.Printf("FOLDER_PATH: %s", constant.FOLDER_PATH) 102 | log.Printf("CONSUL_PREFIX: %s", constant.CONSUL_PREFIX) 103 | log.Printf("------------------------\n") 104 | } 105 | -------------------------------------------------------------------------------- /cmd/server/manager/manager.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 5 | ) 6 | 7 | var ( 8 | nonceMap = make(map[string]*v2.DiscoveryResponse) 9 | versionMap = make(map[string]bool) 10 | lastUpdatedVersion string 11 | ) 12 | 13 | func IsACK(req *v2.DiscoveryRequest) bool { 14 | if _, ok := nonceMap[req.ResponseNonce]; ok { 15 | return true 16 | } 17 | return false 18 | } 19 | 20 | func IsOutDated(versionInfo string) bool { 21 | if _, ok := versionMap[versionInfo]; ok { 22 | return false 23 | } 24 | return true 25 | } 26 | 27 | func UpdateMap(resp *v2.DiscoveryResponse) { 28 | nonceMap[resp.Nonce] = resp 29 | lastUpdatedVersion = resp.VersionInfo 30 | } 31 | -------------------------------------------------------------------------------- /cmd/server/mapper/clusterMapper.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/util" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "runtime/debug" 9 | "strings" 10 | "time" 11 | 12 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 13 | "github.com/envoyproxy/go-control-plane/pkg/cache" 14 | 15 | envoy_api_v2_auth "github.com/envoyproxy/go-control-plane/envoy/api/v2/auth" 16 | envoy_api_v2_cluster "github.com/envoyproxy/go-control-plane/envoy/api/v2/cluster" 17 | envoy_api_v2_core1 "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 18 | "github.com/gogo/protobuf/proto" 19 | "github.com/gogo/protobuf/types" 20 | ) 21 | 22 | type ClusterMapper struct{} 23 | 24 | func buildDnsType(rawObj map[string]interface{}) v2.Cluster_DiscoveryType { 25 | typeStr := getString(rawObj, "type") 26 | typeStr = strings.ToUpper(typeStr) 27 | typeID := v2.Cluster_DiscoveryType_value[typeStr] 28 | return v2.Cluster_DiscoveryType(typeID) 29 | } 30 | 31 | func buildLbPolicy(rawObj map[string]interface{}) v2.Cluster_LbPolicy { 32 | lbID := v2.Cluster_LbPolicy_value[getString(rawObj, "lb_policy")] 33 | return v2.Cluster_LbPolicy(lbID) 34 | } 35 | 36 | func buildHosts(rawObj interface{}) ([]*envoy_api_v2_core1.Address, error) { 37 | if rawObj == nil { 38 | return make([]*envoy_api_v2_core1.Address, 0), nil 39 | } 40 | 41 | rawHosts := toArray(rawObj) 42 | list := make([]*envoy_api_v2_core1.Address, len(rawHosts)) 43 | 44 | for i, row := range rawHosts { 45 | rowMap := row.(map[string]interface{}) 46 | res, err := buildHost(rowMap) 47 | if err != nil { 48 | return nil, err 49 | } 50 | list[i] = &res 51 | } 52 | return list, nil 53 | } 54 | 55 | func buildHost(rowMap map[string]interface{}) (envoy_api_v2_core1.Address, error) { 56 | addrMap := rowMap["socket_address"].(map[string]interface{}) 57 | 58 | port, err := getUInt(addrMap, "port_value") 59 | if err != nil { 60 | return envoy_api_v2_core1.Address{}, err 61 | } 62 | 63 | res := envoy_api_v2_core1.Address{ 64 | Address: &envoy_api_v2_core1.Address_SocketAddress{ 65 | SocketAddress: &envoy_api_v2_core1.SocketAddress{ 66 | Protocol: envoy_api_v2_core1.TCP, 67 | Address: getString(addrMap, "address"), 68 | PortSpecifier: &envoy_api_v2_core1.SocketAddress_PortValue{ 69 | PortValue: port, 70 | }, 71 | }, 72 | }, 73 | } 74 | return res, nil 75 | } 76 | 77 | func BuildDuration(str string) time.Duration { 78 | res, err := time.ParseDuration(str) 79 | if err != nil { 80 | log.Printf("Error parsing string to duration %s\n", str) 81 | log.Println(err) 82 | panic("Error parsing duration") 83 | } 84 | return res 85 | } 86 | 87 | func buildHttp2ProtocolOptions(rawObj interface{}) *envoy_api_v2_core1.Http2ProtocolOptions { 88 | if rawObj == nil { 89 | return nil 90 | } 91 | objMap := toMap(rawObj) 92 | http2 := envoy_api_v2_core1.Http2ProtocolOptions{} 93 | 94 | if keyExists(objMap, "hpack_table_size") { 95 | val, err := getUIntValue(objMap, "hpack_table_size") 96 | util.CheckAndPanic(err) 97 | http2.HpackTableSize = &val 98 | } 99 | 100 | if keyExists(objMap, "max_concurrent_streams") { 101 | val, err := getUIntValue(objMap, "max_concurrent_streams") 102 | util.CheckAndPanic(err) 103 | http2.MaxConcurrentStreams = &val 104 | } 105 | 106 | if keyExists(objMap, "initial_stream_window_size") { 107 | val, err := getUIntValue(objMap, "initial_stream_window_size") 108 | util.CheckAndPanic(err) 109 | http2.InitialStreamWindowSize = &val 110 | } 111 | 112 | if keyExists(objMap, "initial_connection_window_size") { 113 | val, err := getUIntValue(objMap, "initial_connection_window_size") 114 | util.CheckAndPanic(err) 115 | http2.InitialConnectionWindowSize = &val 116 | } 117 | 118 | return &http2 119 | } 120 | 121 | func GetConfigSourceType(typeVal string) envoy_api_v2_core1.ApiConfigSource_ApiType { 122 | id := envoy_api_v2_core1.ApiConfigSource_ApiType_value[typeVal] 123 | return envoy_api_v2_core1.ApiConfigSource_ApiType(id) 124 | } 125 | 126 | func buildEdsClusterConfig(rawObj interface{}) *v2.Cluster_EdsClusterConfig { 127 | if rawObj == nil { 128 | return nil 129 | } 130 | 131 | eds_cluster_config := toMap(rawObj) 132 | eds_config := toMap(eds_cluster_config["eds_config"]) 133 | api_config_source := toMap(eds_config["api_config_source"]) 134 | grpc_services := toArray(api_config_source["grpc_services"]) 135 | 136 | resGrpcServices := make([]*envoy_api_v2_core1.GrpcService, len(grpc_services)) 137 | 138 | for i, grpc_service := range grpc_services { 139 | objMap := toMap(grpc_service) 140 | envoy_grpc := toMap(objMap["envoy_grpc"]) 141 | resGrpcServices[i] = &envoy_api_v2_core1.GrpcService{ 142 | TargetSpecifier: &envoy_api_v2_core1.GrpcService_EnvoyGrpc_{ 143 | EnvoyGrpc: &envoy_api_v2_core1.GrpcService_EnvoyGrpc{ 144 | ClusterName: getString(envoy_grpc, "cluster_name"), 145 | }, 146 | }, 147 | } 148 | } 149 | 150 | return &v2.Cluster_EdsClusterConfig{ 151 | EdsConfig: &envoy_api_v2_core1.ConfigSource{ 152 | ConfigSourceSpecifier: &envoy_api_v2_core1.ConfigSource_ApiConfigSource{ 153 | ApiConfigSource: &envoy_api_v2_core1.ApiConfigSource{ 154 | ApiType: GetConfigSourceType(getString(api_config_source, "api_type")), 155 | GrpcServices: resGrpcServices, 156 | }, 157 | }, 158 | }, 159 | } 160 | } 161 | 162 | func buildThresholds(rawObj interface{}) []*envoy_api_v2_cluster.CircuitBreakers_Thresholds { 163 | if rawObj == nil { 164 | return nil 165 | } 166 | objArr := toArray(rawObj) 167 | thresholds := make([]*envoy_api_v2_cluster.CircuitBreakers_Thresholds, len(objArr)) 168 | 169 | for i, rawThreshold := range objArr { 170 | thresholdMap := toMap(rawThreshold) 171 | priorityId := envoy_api_v2_core1.RoutingPriority_value[getString(thresholdMap, "priority")] 172 | 173 | threshold := envoy_api_v2_cluster.CircuitBreakers_Thresholds{ 174 | Priority: envoy_api_v2_core1.RoutingPriority(priorityId), 175 | } 176 | if thresholdMap["max_connections"] != nil { 177 | val, err := getUIntValue(thresholdMap, "max_connections") 178 | util.CheckAndPanic(err) 179 | threshold.MaxConnections = &val 180 | } 181 | if thresholdMap["max_pending_requests"] != nil { 182 | val, err := getUIntValue(thresholdMap, "max_pending_requests") 183 | util.CheckAndPanic(err) 184 | threshold.MaxPendingRequests = &val 185 | } 186 | if thresholdMap["max_requests"] != nil { 187 | val, err := getUIntValue(thresholdMap, "max_requests") 188 | util.CheckAndPanic(err) 189 | threshold.MaxRequests = &val 190 | } 191 | if thresholdMap["max_retries"] != nil { 192 | val, err := getUIntValue(thresholdMap, "max_retries") 193 | util.CheckAndPanic(err) 194 | threshold.MaxRetries = &val 195 | } 196 | thresholds[i] = &threshold 197 | } 198 | return thresholds 199 | } 200 | 201 | func buildCircuitBreakers(rawObj interface{}) *envoy_api_v2_cluster.CircuitBreakers { 202 | if rawObj == nil { 203 | return nil 204 | } 205 | objMap := toMap(rawObj) 206 | return &envoy_api_v2_cluster.CircuitBreakers{ 207 | Thresholds: buildThresholds(objMap["thresholds"]), 208 | } 209 | } 210 | 211 | func buildClusterTLSContext(rawObj interface{}) *envoy_api_v2_auth.UpstreamTlsContext { 212 | if rawObj == nil { 213 | return nil 214 | } 215 | tlsCtxMap := toMap(rawObj) 216 | commonTlsCtxMap := toMap(tlsCtxMap["common_tls_context"]) 217 | upstreamTlsCtx := &envoy_api_v2_auth.UpstreamTlsContext{ 218 | CommonTlsContext: &envoy_api_v2_auth.CommonTlsContext{ 219 | TlsCertificates: buildTlsCerts(commonTlsCtxMap["tls_certificates"]), 220 | AlpnProtocols: buildAlpnProtocol(commonTlsCtxMap["alpn_protocols"]), 221 | }, 222 | } 223 | if keyExists(tlsCtxMap, "sni") { 224 | upstreamTlsCtx.Sni = getString(tlsCtxMap, "sni") 225 | } 226 | return upstreamTlsCtx 227 | } 228 | 229 | func (c *ClusterMapper) GetCluster(rawObj interface{}) (retCluster v2.Cluster, retErr error) { 230 | var rawObjMap map[string]interface{} 231 | if rawObj != nil { 232 | rawObjMap = toMap(rawObj) 233 | } else { 234 | return v2.Cluster{}, nil 235 | } 236 | 237 | var clusterObj = v2.Cluster{} 238 | 239 | clusterObj.Name = rawObjMap["name"].(string) 240 | cxTimeout := BuildDuration(getString(rawObjMap, "connect_timeout")) 241 | clusterObj.ConnectTimeout = cxTimeout 242 | clusterObj.Type = buildDnsType(rawObjMap) 243 | clusterObj.LbPolicy = buildLbPolicy(rawObjMap) 244 | clusterObj.Http2ProtocolOptions = buildHttp2ProtocolOptions(rawObjMap["http2_protocol_options"]) 245 | clusterObj.EdsClusterConfig = buildEdsClusterConfig(rawObjMap["eds_cluster_config"]) 246 | clusterObj.CircuitBreakers = buildCircuitBreakers(rawObjMap["circuit_breakers"]) 247 | clusterObj.TlsContext = buildClusterTLSContext(rawObjMap["tls_context"]) 248 | 249 | hosts, err := buildHosts(rawObjMap["hosts"]) 250 | if err != nil { 251 | log.Panic(err) 252 | } 253 | clusterObj.Hosts = hosts 254 | return clusterObj, nil 255 | } 256 | 257 | func (c *ClusterMapper) GetClusters(clusterJson string) (retCluster []*v2.Cluster, retErr error) { 258 | defer func() { 259 | if r := recover(); r != nil { 260 | log.Println("*********************************") 261 | log.Printf("Recovered %s from %s: %s\n", "GetClusters", r, debug.Stack()) 262 | log.Println("*********************************") 263 | retErr = errors.New(fmt.Sprintf("%s", r)) 264 | } 265 | }() 266 | 267 | var rawArr []interface{} 268 | // err := json.Unmarshal([]byte(clusterJson), &rawArr) 269 | // if err != nil { 270 | // panic(err) 271 | // } 272 | rawArr = util.ImportJsonOrYaml(clusterJson) 273 | 274 | var clusters = make([]*v2.Cluster, len(rawArr)) 275 | for i, rawCluster := range rawArr { 276 | val, err := c.GetCluster(rawCluster) 277 | if err != nil { 278 | panic(err) 279 | } 280 | clusters[i] = &val 281 | } 282 | return clusters, nil 283 | } 284 | 285 | func (c *ClusterMapper) GetResources(configJson string) ([]types.Any, error) { 286 | typeUrl := cache.ClusterType 287 | 288 | clusters, err := c.GetClusters(configJson) 289 | if err != nil { 290 | log.Printf("Error parsing cluster config") 291 | return nil, err 292 | } 293 | 294 | resources := make([]types.Any, len(clusters)) 295 | 296 | for i, cluster := range clusters { 297 | data, err := proto.Marshal(cluster) 298 | if err != nil { 299 | log.Printf("Error marshalling cluster...\n") 300 | log.Panic(err) 301 | } 302 | resources[i] = types.Any{ 303 | Value: data, 304 | TypeUrl: typeUrl, 305 | } 306 | } 307 | 308 | return resources, err 309 | } 310 | 311 | func check(err error) { 312 | if err != nil { 313 | panic(err) 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /cmd/server/mapper/clusterMapper_test.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 8 | envoy_api_v2_core1 "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 9 | "github.com/go-test/deep" 10 | ) 11 | 12 | var clusterMapper ClusterMapper 13 | 14 | func init() { 15 | clusterMapper = ClusterMapper{} 16 | } 17 | func TestClusterMapper_GetCluster(t *testing.T) { 18 | json := ` 19 | { 20 | "name": "bifrost", 21 | "connect_timeout": "5s", 22 | "type": "STRICT_DNS", 23 | "lb_policy": "ROUND_ROBIN", 24 | "hosts": [{ 25 | "socket_address": { 26 | "address": "127.0.0.1", 27 | "port_value": 1234 28 | } 29 | }] 30 | } 31 | ` 32 | 33 | expectedObj := v2.Cluster{ 34 | Name: "bifrost", 35 | ConnectTimeout: 5 * time.Second, 36 | Type: v2.Cluster_STRICT_DNS, 37 | LbPolicy: v2.Cluster_ROUND_ROBIN, 38 | Hosts: []*envoy_api_v2_core1.Address{ 39 | {&envoy_api_v2_core1.Address_SocketAddress{ 40 | SocketAddress: &envoy_api_v2_core1.SocketAddress{ 41 | Address: "127.0.0.1", 42 | PortSpecifier: &envoy_api_v2_core1.SocketAddress_PortValue{PortValue: 1234}, 43 | }, 44 | }}, 45 | }, 46 | } 47 | actualObj, _ := clusterMapper.GetCluster(json) 48 | 49 | if diff := deep.Equal(actualObj, &expectedObj); diff != nil { 50 | t.Error(diff) 51 | } 52 | } 53 | 54 | func TestClusterMapper_BuildDuration(t *testing.T) { 55 | res := BuildDuration("5s") 56 | expected := 5 * time.Second 57 | if res != expected { 58 | t.Errorf("incorrect time parse: %d, want: %d.", res, expected) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cmd/server/mapper/common.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "strconv" 7 | 8 | google_protobuf1 "github.com/gogo/protobuf/types" 9 | ) 10 | 11 | func getInt(obj map[string]interface{}, key string) (int, error) { 12 | switch t := obj[key].(type) { 13 | case string: 14 | val, err := strconv.Atoi(obj[key].(string)) 15 | if err != nil { 16 | log.Printf("Error parsing int %s\n", obj[key].(string)) 17 | return 0, err 18 | } 19 | return val, err 20 | case float64: 21 | val := int(obj[key].(float64)) 22 | return val, nil 23 | default: 24 | log.Printf("Unknown field type for value %+v and type %s", obj[key], t) 25 | return 0, errors.New("Unable to parse") 26 | } 27 | } 28 | 29 | func getUInt(obj map[string]interface{}, key string) (uint32, error) { 30 | val, err := getInt(obj, key) 31 | if err != nil { 32 | return 0, err 33 | } 34 | return uint32(val), err 35 | } 36 | 37 | func getUIntValue(obj map[string]interface{}, key string) (google_protobuf1.UInt32Value, error) { 38 | val, err := getInt(obj, key) 39 | if err != nil { 40 | return google_protobuf1.UInt32Value{}, err 41 | } 42 | uVal := uint32(val) 43 | return google_protobuf1.UInt32Value{Value: uVal}, err 44 | } 45 | 46 | func getString(obj map[string]interface{}, key string) string { 47 | return obj[key].(string) 48 | } 49 | 50 | func getBoolean(obj map[string]interface{}, key string) bool { 51 | return obj[key].(bool) 52 | } 53 | 54 | func getBoolValue(obj map[string]interface{}, key string) google_protobuf1.BoolValue { 55 | return google_protobuf1.BoolValue{ 56 | Value: getBoolean(obj, key), 57 | } 58 | } 59 | 60 | func getStringArray(obj map[string]interface{}, key string) []string { 61 | arr := obj[key].([]interface{}) 62 | res := make([]string, len(arr)) 63 | for i, value := range arr { 64 | res[i] = value.(string) 65 | } 66 | return res 67 | } 68 | 69 | func getFloat(obj map[string]interface{}, key string) float64 { 70 | return obj[key].(float64) 71 | } 72 | 73 | func toMap(obj interface{}) map[string]interface{} { 74 | return obj.(map[string]interface{}) 75 | } 76 | 77 | func toArray(obj interface{}) []interface{} { 78 | return obj.([]interface{}) 79 | } 80 | 81 | func keyExists(objMap map[string]interface{}, key string) bool { 82 | return objMap[key] != nil 83 | } 84 | -------------------------------------------------------------------------------- /cmd/server/mapper/endpointMapper.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | xdsUtil "Envoy-Pilot/cmd/server/util" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "runtime/debug" 9 | 10 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 11 | "github.com/envoyproxy/go-control-plane/pkg/cache" 12 | "github.com/gogo/protobuf/proto" 13 | "github.com/gogo/protobuf/types" 14 | 15 | pilot_util "Envoy-Pilot/cmd/server/util" 16 | 17 | envoy_api_v2_core1 "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 18 | envoy_api_v2_endpoint "github.com/envoyproxy/go-control-plane/envoy/api/v2/endpoint" 19 | ) 20 | 21 | type EndpointMapper struct{} 22 | 23 | func (e *EndpointMapper) GetSocketAddress(rawObj interface{}) *envoy_api_v2_core1.Address { 24 | if rawObj == nil { 25 | return &envoy_api_v2_core1.Address{} 26 | } 27 | addressMap := toMap(rawObj) 28 | 29 | if addressMap == nil { 30 | return &envoy_api_v2_core1.Address{} 31 | } 32 | 33 | socketAddressMap := toMap(addressMap["socket_address"]) 34 | 35 | if addressMap == nil { 36 | return &envoy_api_v2_core1.Address{} 37 | } 38 | 39 | port, err := getUInt(socketAddressMap, "port_value") 40 | pilot_util.CheckAndPanic(err) 41 | 42 | return &envoy_api_v2_core1.Address{&envoy_api_v2_core1.Address_SocketAddress{ 43 | SocketAddress: &envoy_api_v2_core1.SocketAddress{ 44 | Address: getString(socketAddressMap, "address"), 45 | PortSpecifier: &envoy_api_v2_core1.SocketAddress_PortValue{ 46 | PortValue: port, 47 | }, 48 | }, 49 | }} 50 | } 51 | 52 | func (e *EndpointMapper) GetEndpoint(rawObj interface{}) *envoy_api_v2_endpoint.Endpoint { 53 | if rawObj == nil { 54 | return &envoy_api_v2_endpoint.Endpoint{} 55 | } 56 | endpointMap := toMap(rawObj) 57 | rawAddress := toMap(endpointMap["address"]) 58 | return &envoy_api_v2_endpoint.Endpoint{ 59 | Address: e.GetSocketAddress(rawAddress), 60 | } 61 | } 62 | 63 | func (e *EndpointMapper) GetLbEndpoint(rawObj interface{}) envoy_api_v2_endpoint.LbEndpoint { 64 | if rawObj == nil { 65 | return envoy_api_v2_endpoint.LbEndpoint{} 66 | } 67 | lbEndpointMap := toMap(rawObj) 68 | 69 | return envoy_api_v2_endpoint.LbEndpoint{ 70 | Endpoint: e.GetEndpoint(lbEndpointMap["endpoint"]), 71 | } 72 | } 73 | 74 | func (e *EndpointMapper) GetLbEndpoints(rawObj interface{}) []envoy_api_v2_endpoint.LbEndpoint { 75 | if rawObj == nil { 76 | return make([]envoy_api_v2_endpoint.LbEndpoint, 0) 77 | } 78 | rawLbEndpoints := toArray(rawObj) 79 | lbEndpoints := make([]envoy_api_v2_endpoint.LbEndpoint, len(rawLbEndpoints)) 80 | 81 | for i, rawLbEndpoint := range rawLbEndpoints { 82 | lbEndpoints[i] = e.GetLbEndpoint(rawLbEndpoint) 83 | } 84 | 85 | return lbEndpoints 86 | } 87 | 88 | func (e *EndpointMapper) GetLocalityLbEndpoints(rawObj interface{}) []envoy_api_v2_endpoint.LocalityLbEndpoints { 89 | if rawObj == nil { 90 | return make([]envoy_api_v2_endpoint.LocalityLbEndpoints, 0) 91 | } 92 | rawLocalityLbEndpoints := toArray(rawObj) 93 | localityLbEndpoints := make([]envoy_api_v2_endpoint.LocalityLbEndpoints, len(rawLocalityLbEndpoints)) 94 | 95 | for i, rawlocalityLbEndpoint := range rawLocalityLbEndpoints { 96 | localityLbEndpointMap := toMap(rawlocalityLbEndpoint) 97 | localityLbEndpoints[i] = envoy_api_v2_endpoint.LocalityLbEndpoints{ 98 | LbEndpoints: e.GetLbEndpoints(getString(localityLbEndpointMap, "lb_endpoints")), 99 | } 100 | } 101 | return localityLbEndpoints 102 | } 103 | 104 | func (e *EndpointMapper) GetClusterLoadAssignment(rawObj interface{}) (retEndpoints *v2.ClusterLoadAssignment, retErr error) { 105 | if rawObj == nil { 106 | return &v2.ClusterLoadAssignment{}, nil 107 | } 108 | objMap := toMap(rawObj) 109 | cla := &v2.ClusterLoadAssignment{ 110 | ClusterName: getString(objMap, "cluster_name"), 111 | } 112 | return cla, nil 113 | } 114 | 115 | func (e *EndpointMapper) GetClusterLoadAssignments(endpointsJson string) (retEndpoints []*v2.ClusterLoadAssignment, retErr error) { 116 | defer func() { 117 | if r := recover(); r != nil { 118 | log.Println("*********************************") 119 | log.Printf("Recovered %s from %s: %s\n", "GetEndpoints", r, debug.Stack()) 120 | log.Println("*********************************") 121 | retErr = errors.New(fmt.Sprintf("%s", r)) 122 | } 123 | }() 124 | var rawArr []interface{} 125 | // err := json.Unmarshal([]byte(endpointsJson), &rawArr) 126 | // if err != nil { 127 | // panic(err) 128 | // } 129 | rawArr = xdsUtil.ImportJsonOrYaml(endpointsJson) 130 | 131 | var endpoints = make([]*v2.ClusterLoadAssignment, len(rawArr)) 132 | for i, rawEndpoint := range rawArr { 133 | val, err := e.GetClusterLoadAssignment(rawEndpoint) 134 | if err != nil { 135 | panic(err) 136 | } 137 | endpoints[i] = val 138 | } 139 | return endpoints, nil 140 | } 141 | 142 | func (e *EndpointMapper) GetResources(configJson string) ([]types.Any, error) { 143 | typeUrl := cache.EndpointType 144 | 145 | endpoints, err := e.GetClusterLoadAssignments(configJson) 146 | if err != nil { 147 | log.Printf("Error parsing endpoint config") 148 | return nil, err 149 | } 150 | resources := make([]types.Any, len(endpoints)) 151 | 152 | for i, endpoint := range endpoints { 153 | data, err := proto.Marshal(endpoint) 154 | if err != nil { 155 | log.Printf("Error marshalling endpoint config") 156 | log.Panic(err) 157 | } 158 | 159 | resources[i] = types.Any{ 160 | Value: data, 161 | TypeUrl: typeUrl, 162 | } 163 | } 164 | 165 | return resources, nil 166 | } 167 | -------------------------------------------------------------------------------- /cmd/server/mapper/listenerMapper.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | xdsUtil "Envoy-Pilot/cmd/server/util" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "runtime/debug" 9 | "strings" 10 | "time" 11 | 12 | "github.com/envoyproxy/go-control-plane/pkg/cache" 13 | "github.com/envoyproxy/go-control-plane/pkg/util" 14 | 15 | "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 16 | 17 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 18 | envoy_api_v2_auth "github.com/envoyproxy/go-control-plane/envoy/api/v2/auth" 19 | envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 20 | "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener" 21 | envoy_api_v2_route "github.com/envoyproxy/go-control-plane/envoy/api/v2/route" 22 | als "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v2" 23 | alf "github.com/envoyproxy/go-control-plane/envoy/config/filter/accesslog/v2" 24 | hc "github.com/envoyproxy/go-control-plane/envoy/config/filter/http/health_check/v2" 25 | hcm "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2" 26 | "github.com/gogo/protobuf/proto" 27 | "github.com/gogo/protobuf/types" 28 | ) 29 | 30 | type ListenerMapper struct{} 31 | 32 | func buildRds(rawConfigObj interface{}) hcm.HttpConnectionManager_Rds { 33 | var rawConfig map[string]interface{} 34 | if rawConfigObj != nil { 35 | rawConfig = rawConfigObj.(map[string]interface{}) 36 | } else { 37 | return hcm.HttpConnectionManager_Rds{} 38 | } 39 | rdsSource := core.ConfigSource{} 40 | configSource := rawConfig["config_source"].(map[string]interface{}) 41 | sourceMap := configSource["api_config_source"].(map[string]interface{}) 42 | grpcServices := sourceMap["grpc_services"].([]interface{}) 43 | 44 | resGrpcServices := make([]*core.GrpcService, len(grpcServices)) 45 | for i, grpcService := range grpcServices { 46 | grpcServiceMap := toMap(grpcService) 47 | envoyGrpc := toMap(grpcServiceMap["envoy_grpc"]) 48 | resGrpcServices[i] = &core.GrpcService{ 49 | TargetSpecifier: &core.GrpcService_EnvoyGrpc_{ 50 | EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: getString(envoyGrpc, "cluster_name")}, 51 | }, 52 | } 53 | } 54 | 55 | rdsSource.ConfigSourceSpecifier = &core.ConfigSource_ApiConfigSource{ 56 | ApiConfigSource: &core.ApiConfigSource{ 57 | ApiType: core.ApiConfigSource_GRPC, 58 | GrpcServices: resGrpcServices, 59 | }, 60 | } 61 | 62 | return hcm.HttpConnectionManager_Rds{ 63 | Rds: &hcm.Rds{ 64 | ConfigSource: rdsSource, 65 | RouteConfigName: getString(rawConfig, "route_config_name"), 66 | }, 67 | } 68 | } 69 | 70 | func buildWeightedClusters(rawObj interface{}) *envoy_api_v2_route.WeightedCluster { 71 | if rawObj == nil { 72 | return nil 73 | } 74 | wcMap := toMap(rawObj) 75 | totalWeight, err := getUIntValue(wcMap, "total_weight") 76 | xdsUtil.Check(err) 77 | rkey := getString(wcMap, "runtime_key_prefix") 78 | 79 | rawClusters := toArray(wcMap["clusters"]) 80 | res := make([]*envoy_api_v2_route.WeightedCluster_ClusterWeight, len(rawClusters)) 81 | for i, rc := range rawClusters { 82 | clusterMap := toMap(rc) 83 | w, err := getUIntValue(clusterMap, "weight") 84 | xdsUtil.Check(err) 85 | res[i] = &envoy_api_v2_route.WeightedCluster_ClusterWeight{ 86 | Name: getString(clusterMap, "name"), 87 | Weight: &w, 88 | } 89 | } 90 | return &envoy_api_v2_route.WeightedCluster{ 91 | Clusters: res, 92 | TotalWeight: &totalWeight, 93 | RuntimeKeyPrefix: rkey, 94 | } 95 | } 96 | 97 | func buildRoutes(rawObj interface{}) []envoy_api_v2_route.Route { 98 | if rawObj == nil { 99 | return make([]envoy_api_v2_route.Route, 0) 100 | } 101 | routes := rawObj.([]interface{}) 102 | res := make([]envoy_api_v2_route.Route, len(routes)) 103 | 104 | for i, route := range routes { 105 | routeMap := toMap(route) 106 | matchMap := toMap(routeMap["match"]) 107 | routeRouteMap := toMap(routeMap["route"]) 108 | var timeout *time.Duration 109 | if keyExists(routeRouteMap, "timeout") { 110 | res, err := time.ParseDuration(getString(routeRouteMap, "timeout")) 111 | xdsUtil.Check(err) 112 | timeout = &res 113 | } 114 | 115 | tmpRoute := envoy_api_v2_route.RouteAction{} 116 | if keyExists(routeRouteMap, "cluster") { 117 | tmpRoute.ClusterSpecifier = &envoy_api_v2_route.RouteAction_Cluster{ 118 | Cluster: getString(routeRouteMap, "cluster"), 119 | } 120 | } 121 | 122 | if keyExists(routeRouteMap, "weighted_clusters") { 123 | tmpRoute.ClusterSpecifier = &envoy_api_v2_route.RouteAction_WeightedClusters{ 124 | WeightedClusters: buildWeightedClusters(routeRouteMap["weighted_clusters"]), 125 | } 126 | } 127 | 128 | if timeout != nil { 129 | tmpRoute.Timeout = timeout 130 | } 131 | 132 | res[i] = envoy_api_v2_route.Route{ 133 | Match: envoy_api_v2_route.RouteMatch{ 134 | PathSpecifier: &envoy_api_v2_route.RouteMatch_Prefix{ 135 | Prefix: getString(matchMap, "prefix"), 136 | }, 137 | }, 138 | Action: &envoy_api_v2_route.Route_Route{ 139 | Route: &tmpRoute, 140 | }, 141 | } 142 | } 143 | return res 144 | } 145 | 146 | func buildVHosts(rawVHosts interface{}) []envoy_api_v2_route.VirtualHost { 147 | if rawVHosts == nil { 148 | return make([]envoy_api_v2_route.VirtualHost, 0) 149 | } 150 | vhosts := rawVHosts.([]interface{}) 151 | res := make([]envoy_api_v2_route.VirtualHost, len(vhosts)) 152 | 153 | for i, vhost := range vhosts { 154 | vhostMap := toMap(vhost) 155 | res[i] = envoy_api_v2_route.VirtualHost{ 156 | Name: getString(vhostMap, "name"), 157 | Domains: getStringArray(vhostMap, "domains"), 158 | Routes: buildRoutes(vhostMap["routes"]), 159 | } 160 | } 161 | 162 | return res 163 | } 164 | 165 | func BuildRouteConfig(rawObj interface{}) hcm.HttpConnectionManager_RouteConfig { 166 | var routeConfigMap map[string]interface{} 167 | if rawObj != nil { 168 | routeConfigMap = rawObj.(map[string]interface{}) 169 | } else { 170 | return hcm.HttpConnectionManager_RouteConfig{} 171 | } 172 | 173 | rConfig := v2.RouteConfiguration{ 174 | Name: getString(routeConfigMap, "name"), 175 | VirtualHosts: buildVHosts(routeConfigMap["virtual_hosts"]), 176 | } 177 | 178 | return hcm.HttpConnectionManager_RouteConfig{ 179 | RouteConfig: &rConfig, 180 | } 181 | } 182 | 183 | func buildAccessLog(rawAlsArrayObj interface{}) []*alf.AccessLog { 184 | var rawAlsArray []interface{} 185 | if rawAlsArrayObj != nil { 186 | rawAlsArray = rawAlsArrayObj.([]interface{}) 187 | } else { 188 | return make([]*alf.AccessLog, 0) 189 | } 190 | res := make([]*alf.AccessLog, len(rawAlsArray)) 191 | for i, rawAls := range rawAlsArray { 192 | alsMap := rawAls.(map[string]interface{}) 193 | configMap := alsMap["config"].(map[string]interface{}) 194 | alsConfig := &als.FileAccessLog{ 195 | Path: getString(configMap, "path"), 196 | Format: getString(configMap, "format"), 197 | } 198 | 199 | alsConfigPbst, err := util.MessageToStruct(alsConfig) 200 | if err != nil { 201 | panic(err) 202 | } 203 | als := &alf.AccessLog{ 204 | Name: util.FileAccessLog, 205 | Config: alsConfigPbst, 206 | } 207 | res[i] = als 208 | } 209 | return res 210 | } 211 | 212 | type httpFilterConfig struct { 213 | PassThroughMode bool 214 | Endpoint string 215 | } 216 | 217 | func (m *httpFilterConfig) Reset() { *m = httpFilterConfig{} } 218 | func (m *httpFilterConfig) String() string { return proto.CompactTextString(m) } 219 | func (*httpFilterConfig) ProtoMessage() {} 220 | 221 | func (m *httpFilterConfig) GetPassThroughMode() bool { 222 | if m != nil { 223 | return m.PassThroughMode 224 | } 225 | return false 226 | } 227 | 228 | func (m *httpFilterConfig) GetEndpoint() string { 229 | if m != nil { 230 | return m.Endpoint 231 | } 232 | return "" 233 | } 234 | 235 | func buildHttpFilter(rawConfig interface{}) []*hcm.HttpFilter { 236 | if rawConfig == nil { 237 | return make([]*hcm.HttpFilter, 0) 238 | } 239 | filters := rawConfig.([]interface{}) 240 | res := make([]*hcm.HttpFilter, len(filters)) 241 | 242 | for i, filter := range filters { 243 | filterMap := toMap(filter) 244 | 245 | fc := &hcm.HttpFilter{ 246 | Name: getString(filterMap, "name"), 247 | } 248 | if filterMap["config"] != nil { 249 | configMap := toMap(filterMap["config"]) 250 | hfconfig2 := hc.HealthCheck{ 251 | PassThroughMode: &types.BoolValue{Value: getBoolean(configMap, "pass_through_mode")}, 252 | Endpoint: getString(configMap, "endpoint"), 253 | } 254 | pbConfig2, err := util.MessageToStruct(&hfconfig2) 255 | if err != nil { 256 | log.Panic(err) 257 | } 258 | fc.Config = pbConfig2 259 | } 260 | 261 | res[i] = fc 262 | } 263 | 264 | return res 265 | } 266 | 267 | func buildTracing(rawObj interface{}) *hcm.HttpConnectionManager_Tracing { 268 | if rawObj == nil { 269 | return nil 270 | } 271 | // log.Printf("Tracing: %+v\n", rawObj) 272 | objMap := toMap(rawObj) 273 | // log.Printf("Tracing: %+v\n", objMap) 274 | 275 | operationName := getString(objMap, "operation_name") 276 | operationName = strings.ToUpper(operationName) 277 | operationId := hcm.HttpConnectionManager_Tracing_OperationName_value[operationName] 278 | return &hcm.HttpConnectionManager_Tracing{ 279 | OperationName: hcm.HttpConnectionManager_Tracing_OperationName(operationId), 280 | } 281 | } 282 | 283 | func buildHttpConnectionManager(rawConfig map[string]interface{}) hcm.HttpConnectionManager { 284 | als := buildAccessLog(rawConfig["access_log"]) 285 | codec := hcm.HttpConnectionManager_CodecType_value[getString(rawConfig, "codec_type")] 286 | manager := hcm.HttpConnectionManager{ 287 | CodecType: hcm.HttpConnectionManager_CodecType(codec), 288 | StatPrefix: getString(rawConfig, "stat_prefix"), 289 | HttpFilters: buildHttpFilter(rawConfig["http_filters"]), 290 | AccessLog: als, 291 | } 292 | 293 | if keyExists(rawConfig, "generate_request_id") { 294 | boolVal := getBoolValue(rawConfig, "generate_request_id") 295 | manager.GenerateRequestId = &boolVal 296 | } 297 | 298 | if keyExists(rawConfig, "tracing") { 299 | manager.Tracing = buildTracing(rawConfig["tracing"]) 300 | } 301 | 302 | if rawConfig["rds"] != nil { 303 | rds := buildRds(rawConfig["rds"]) 304 | manager.RouteSpecifier = &rds 305 | } else if rawConfig["route_config"] != nil { 306 | // routeSpec = buildRds(rawConfig["rds"]) 307 | routeConfig := BuildRouteConfig(rawConfig["route_config"]) 308 | manager.RouteSpecifier = &routeConfig 309 | } else { 310 | panic("Rds or Routeconfig should be present") 311 | } 312 | 313 | return manager 314 | } 315 | 316 | func buildTlsCerts(rawObj interface{}) []*envoy_api_v2_auth.TlsCertificate { 317 | if rawObj == nil { 318 | return make([]*envoy_api_v2_auth.TlsCertificate, 0) 319 | } 320 | // tlsCtxMap := toMap(rawObj) 321 | // rawArr := toArray(tlsCtxMap["tls_certificates"]) 322 | rawArr := toArray(rawObj) 323 | res := make([]*envoy_api_v2_auth.TlsCertificate, len(rawArr)) 324 | for i, rawCert := range rawArr { 325 | certMap := toMap(rawCert) 326 | certChainMap := toMap(certMap["certificate_chain"]) 327 | privateKeyMap := toMap(certMap["private_key"]) 328 | res[i] = &envoy_api_v2_auth.TlsCertificate{ 329 | CertificateChain: &envoy_api_v2_core.DataSource{ 330 | Specifier: &envoy_api_v2_core.DataSource_Filename{ 331 | Filename: getString(certChainMap, "filename"), 332 | }, 333 | }, 334 | PrivateKey: &envoy_api_v2_core.DataSource{ 335 | Specifier: &envoy_api_v2_core.DataSource_Filename{ 336 | Filename: getString(privateKeyMap, "filename"), 337 | }, 338 | }, 339 | } 340 | } 341 | return res 342 | } 343 | 344 | func buildAlpnProtocol(rawObj interface{}) []string { 345 | if rawObj == nil { 346 | return make([]string, 0) 347 | } 348 | res := make([]string, 1) 349 | res[0] = rawObj.(string) 350 | return res 351 | } 352 | 353 | func buildTlsContext(rawObj interface{}) *envoy_api_v2_auth.DownstreamTlsContext { 354 | if rawObj == nil { 355 | return nil 356 | } 357 | tlsCtxMap := toMap(rawObj) 358 | commonTlsCtxMap := toMap(tlsCtxMap["common_tls_context"]) 359 | 360 | return &envoy_api_v2_auth.DownstreamTlsContext{ 361 | CommonTlsContext: &envoy_api_v2_auth.CommonTlsContext{ 362 | TlsCertificates: buildTlsCerts(commonTlsCtxMap["tls_certificates"]), 363 | AlpnProtocols: buildAlpnProtocol(commonTlsCtxMap["alpn_protocols"]), 364 | }, 365 | } 366 | } 367 | 368 | func buildFilterChains(rawFilterChains []interface{}) ([]listener.FilterChain, error) { 369 | pbFilterChains := make([]listener.FilterChain, len(rawFilterChains)) 370 | for i, rawFilterChain := range rawFilterChains { 371 | filterChainMap := rawFilterChain.(map[string]interface{}) 372 | rawFilters := filterChainMap["filters"].([]interface{}) 373 | pbFilters := make([]listener.Filter, len(rawFilters)) 374 | 375 | for j, rawFilter := range rawFilters { 376 | filterMap := rawFilter.(map[string]interface{}) 377 | pbFilter := listener.Filter{} 378 | pbFilter.Name = getString(filterMap, "name") 379 | 380 | manager := buildHttpConnectionManager(filterMap["config"].(map[string]interface{})) 381 | pbst, err := util.MessageToStruct(&manager) 382 | if err != nil { 383 | panic(err) 384 | } 385 | 386 | pbFilter.Config = pbst 387 | pbFilters[j] = pbFilter 388 | } 389 | pbFilterChains[i] = listener.FilterChain{ 390 | Filters: pbFilters, 391 | TlsContext: buildTlsContext(filterChainMap["tls_context"]), 392 | } 393 | } 394 | return pbFilterChains, nil 395 | } 396 | 397 | func (c *ListenerMapper) GetListener(rawObj interface{}) (retListener v2.Listener, retErr error) { 398 | var listenerObj = v2.Listener{} 399 | var rawListener map[string]interface{} 400 | if rawObj == nil { 401 | return listenerObj, nil 402 | } else { 403 | rawListener = rawObj.(map[string]interface{}) 404 | } 405 | 406 | listenerObj.Name = rawListener["name"].(string) 407 | addr, err := buildHost(rawListener["address"].(map[string]interface{})) 408 | if err != nil { 409 | return listenerObj, err 410 | } 411 | listenerObj.Address = addr 412 | filterChains, _ := buildFilterChains(rawListener["filter_chains"].([]interface{})) 413 | listenerObj.FilterChains = filterChains 414 | return listenerObj, nil 415 | } 416 | 417 | func (c *ListenerMapper) GetListeners(listenerJson string) (retListener []v2.Listener, retErr error) { 418 | defer func() { 419 | if r := recover(); r != nil { 420 | log.Println("*********************************") 421 | log.Printf("Recovered %s from %s: %s\n", "GetListeners", r, debug.Stack()) 422 | log.Println("*********************************") 423 | retErr = errors.New(fmt.Sprintf("%s", r)) 424 | } 425 | }() 426 | var rawArr []interface{} 427 | // err := json.Unmarshal([]byte(listenerJson), &rawArr) 428 | // if err != nil { 429 | // panic(err) 430 | // } 431 | rawArr = xdsUtil.ImportJsonOrYaml(listenerJson) 432 | 433 | var listeners = make([]v2.Listener, len(rawArr)) 434 | for i, rawListener := range rawArr { 435 | val, err := c.GetListener(rawListener) 436 | if err != nil { 437 | panic(err) 438 | } 439 | listeners[i] = val 440 | } 441 | return listeners, nil 442 | } 443 | 444 | func (l *ListenerMapper) GetResources(configJson string) ([]types.Any, error) { 445 | typeUrl := cache.ListenerType 446 | 447 | listeners, err := l.GetListeners(configJson) 448 | if err != nil { 449 | log.Printf("Error parsing listener config") 450 | return nil, err 451 | } 452 | resources := make([]types.Any, len(listeners)) 453 | 454 | for i, listener := range listeners { 455 | data, err := proto.Marshal(&listener) 456 | if err != nil { 457 | log.Panic(err) 458 | } 459 | resources[i] = types.Any{ 460 | Value: data, 461 | TypeUrl: typeUrl, 462 | } 463 | } 464 | return resources, nil 465 | } 466 | -------------------------------------------------------------------------------- /cmd/server/mapper/listenerMapper_test.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 9 | "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 10 | envoy_api_v2_core1 "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 11 | "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener" 12 | als "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v2" 13 | alf "github.com/envoyproxy/go-control-plane/envoy/config/filter/accesslog/v2" 14 | hcm "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2" 15 | util "github.com/envoyproxy/go-control-plane/pkg/util" 16 | "github.com/go-test/deep" 17 | "github.com/gogo/protobuf/jsonpb" 18 | ) 19 | 20 | var listenerMapper ListenerMapper 21 | 22 | func init() { 23 | listenerMapper = ListenerMapper{} 24 | } 25 | func TestListenerMapper_GetListener(t *testing.T) { 26 | jsonString := ` 27 | { 28 | "name": "listener_0", 29 | "address": { 30 | "socket_address": { 31 | "address": "127.0.0.1", 32 | "port_value": 10000 33 | } 34 | }, 35 | "filter_chains": [ 36 | { 37 | "filters": [ 38 | { 39 | "name": "envoy.http_connection_manager", 40 | "config": { 41 | "stat_prefix": "ingress_http", 42 | "access_log": [ 43 | { 44 | "name": "envoy.file_access_log", 45 | "config": { 46 | "path": "/dev/stdout", 47 | "format": "some-format" 48 | } 49 | } 50 | ], 51 | "codec_type": "AUTO", 52 | "rds": { 53 | "route_config_name": "local_route", 54 | "config_source": { 55 | "api_config_source": { 56 | "api_type": "GRPC", 57 | "grpc_services": { 58 | "envoy_grpc": { 59 | "cluster_name": "xds_cluster" 60 | } 61 | } 62 | } 63 | } 64 | }, 65 | "http_filters": [ 66 | { 67 | "name": "envoy.router" 68 | } 69 | ] 70 | } 71 | } 72 | ] 73 | } 74 | ] 75 | } 76 | ` 77 | 78 | rdsSource := core.ConfigSource{} 79 | rdsSource.ConfigSourceSpecifier = &core.ConfigSource_ApiConfigSource{ 80 | ApiConfigSource: &core.ApiConfigSource{ 81 | ApiType: core.ApiConfigSource_GRPC, 82 | GrpcServices: []*core.GrpcService{{ 83 | TargetSpecifier: &core.GrpcService_EnvoyGrpc_{ 84 | EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "xds_cluster"}, 85 | }, 86 | }}, 87 | }, 88 | } 89 | 90 | alsConfig := &als.FileAccessLog{ 91 | Path: "/var/access_log.log", 92 | Format: "some-format", 93 | } 94 | 95 | alsConfigPbst, err := util.MessageToStruct(alsConfig) 96 | if err != nil { 97 | panic(err) 98 | } 99 | 100 | manager := &hcm.HttpConnectionManager{ 101 | CodecType: hcm.AUTO, 102 | StatPrefix: "ingress_http", 103 | RouteSpecifier: &hcm.HttpConnectionManager_Rds{ 104 | Rds: &hcm.Rds{ 105 | ConfigSource: rdsSource, 106 | RouteConfigName: "local_route", 107 | }, 108 | }, 109 | HttpFilters: []*hcm.HttpFilter{{ 110 | Name: util.Router, 111 | }}, 112 | AccessLog: []*alf.AccessLog{{ 113 | Name: util.FileAccessLog, 114 | Config: alsConfigPbst, 115 | }}, 116 | } 117 | pbst, err := util.MessageToStruct(manager) 118 | 119 | expectedListener := v2.Listener{ 120 | Name: "listener_0", 121 | Address: envoy_api_v2_core1.Address{ 122 | Address: &envoy_api_v2_core1.Address_SocketAddress{ 123 | SocketAddress: &envoy_api_v2_core1.SocketAddress{ 124 | Address: "127.0.0.1", 125 | PortSpecifier: &envoy_api_v2_core1.SocketAddress_PortValue{PortValue: 1234}, 126 | }, 127 | }, 128 | }, 129 | FilterChains: []listener.FilterChain{ 130 | { 131 | Filters: []listener.Filter{ 132 | { 133 | Name: util.HTTPConnectionManager, 134 | Config: pbst, 135 | }, 136 | }, 137 | }, 138 | }, 139 | } 140 | // _ = jsonString 141 | 142 | actualObj, _ := listenerMapper.GetListener(jsonString) 143 | 144 | marshaler := &jsonpb.Marshaler{} 145 | actJson, err := marshaler.MarshalToString(actualObj) 146 | exJson, err := marshaler.MarshalToString(&expectedListener) 147 | 148 | log.Println("*************") 149 | log.Println(string(actJson)) 150 | log.Println("*************") 151 | log.Println(string(exJson)) 152 | log.Println("*************") 153 | 154 | if diff := deep.Equal(actualObj, &expectedListener); diff != nil { 155 | t.Error(diff) 156 | } 157 | } 158 | func TestListenerMapper_GetListenerWithoutRds(t *testing.T) { 159 | jsonStr := ` 160 | { 161 | "name": "listener_0", 162 | "address": { 163 | "socket_address": { 164 | "address": "0.0.0.0", 165 | "port_value": 80 166 | } 167 | }, 168 | "filter_chains": [ 169 | { 170 | "filters": [ 171 | { 172 | "name": "envoy.http_connection_manager", 173 | "config": { 174 | "stat_prefix": "ingress_http", 175 | "codec_type": "AUTO", 176 | "route_config": { 177 | "name": "local_http_router", 178 | "virtual_hosts": [ 179 | { 180 | "name": "local_service", 181 | "domains": [ 182 | "*" 183 | ], 184 | "routes": [ 185 | { 186 | "match": { 187 | "prefix": "/" 188 | }, 189 | "route": { 190 | "cluster": "app1" 191 | } 192 | } 193 | ] 194 | } 195 | ] 196 | }, 197 | "http_filters": [ 198 | { 199 | "name": "envoy.health_check", 200 | "config": { 201 | "pass_through_mode": false, 202 | "endpoint": "/healthz" 203 | } 204 | }, 205 | { 206 | "name": "envoy.router" 207 | } 208 | ] 209 | } 210 | } 211 | ] 212 | } 213 | ] 214 | } 215 | ` 216 | 217 | actualObj, _ := listenerMapper.GetListener(jsonStr) 218 | marshaler := &jsonpb.Marshaler{} 219 | actJson, err := marshaler.MarshalToString(actualObj) 220 | if err != nil { 221 | log.Panic(err) 222 | } 223 | fmt.Println(actJson) 224 | } 225 | -------------------------------------------------------------------------------- /cmd/server/mapper/mapper.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "fmt" 6 | 7 | "github.com/gogo/protobuf/types" 8 | ) 9 | 10 | type MapperStruct interface { 11 | GetResources(configJson string) ([]types.Any, error) 12 | } 13 | 14 | // GetMapperFor given topic 15 | func GetMapperFor(topic string) MapperStruct { 16 | switch topic { 17 | case constant.SUBSCRIBE_CDS: 18 | return &ClusterMapper{} 19 | case constant.SUBSCRIBE_LDS: 20 | return &ListenerMapper{} 21 | case constant.SUBSCRIBE_RDS: 22 | return &RouteMapper{} 23 | case constant.SUBSCRIBE_EDS: 24 | return &EndpointMapper{} 25 | default: 26 | panic(fmt.Sprintf("No mapper found for type %s\n", topic)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cmd/server/mapper/routeMapper.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "runtime/debug" 8 | 9 | xdsUtil "Envoy-Pilot/cmd/server/util" 10 | 11 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 12 | "github.com/envoyproxy/go-control-plane/pkg/cache" 13 | "github.com/gogo/protobuf/proto" 14 | "github.com/gogo/protobuf/types" 15 | ) 16 | 17 | type RouteMapper struct{} 18 | 19 | func (r *RouteMapper) GetRoute(rawObj interface{}) (retRoutes *v2.RouteConfiguration, retErr error) { 20 | if rawObj == nil { 21 | return &v2.RouteConfiguration{}, nil 22 | } 23 | connManager := BuildRouteConfig(rawObj) 24 | return connManager.RouteConfig, nil 25 | } 26 | 27 | func (r *RouteMapper) GetRoutes(routesJson string) (retRoutes []*v2.RouteConfiguration, retErr error) { 28 | defer func() { 29 | if r := recover(); r != nil { 30 | log.Println("*********************************") 31 | log.Printf("Recovered %s from %s: %s\n", "GetRoutes", r, debug.Stack()) 32 | log.Println("*********************************") 33 | retErr = errors.New(fmt.Sprintf("%s", r)) 34 | } 35 | }() 36 | var rawArr []interface{} 37 | // err := json.Unmarshal([]byte(routesJson), &rawArr) 38 | // if err != nil { 39 | // panic(err) 40 | // } 41 | rawArr = xdsUtil.ImportJsonOrYaml(routesJson) 42 | 43 | var routes = make([]*v2.RouteConfiguration, len(rawArr)) 44 | for i, rawRoute := range rawArr { 45 | val, err := r.GetRoute(rawRoute) 46 | if err != nil { 47 | panic(err) 48 | } 49 | routes[i] = val 50 | } 51 | return routes, nil 52 | } 53 | 54 | func (r *RouteMapper) GetResources(configJson string) ([]types.Any, error) { 55 | typeUrl := cache.RouteType 56 | 57 | routes, err := r.GetRoutes(configJson) 58 | if err != nil { 59 | log.Printf("Error parsing route config") 60 | return nil, err 61 | } 62 | resources := make([]types.Any, len(routes)) 63 | 64 | for i, route := range routes { 65 | data, err := proto.Marshal(route) 66 | if err != nil { 67 | log.Printf("Error marshalling route config") 68 | log.Panic(err) 69 | } 70 | 71 | resources[i] = types.Any{ 72 | Value: data, 73 | TypeUrl: typeUrl, 74 | } 75 | } 76 | 77 | return resources, nil 78 | } 79 | -------------------------------------------------------------------------------- /cmd/server/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/model" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/prometheus/client_golang/prometheus/promhttp" 10 | ) 11 | 12 | // metrics 13 | var METRICS_ACTIVE_CONNECTIONS = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 14 | Name: "xds_active_connections", 15 | Help: "Xds Active Connections", 16 | }, 17 | []string{ 18 | "cluster", 19 | }) 20 | 21 | var METRICS_ACTIVE_SUBSCRIBERS = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 22 | Name: "xds_active_subscribers", 23 | Help: "Xds Active Subscribers By Topic", 24 | }, 25 | []string{ 26 | "cluster", 27 | "type", 28 | }) 29 | 30 | var XDS_UPDATE_COUNTER = prometheus.NewCounterVec(prometheus.CounterOpts{ 31 | Name: "xds_update_counter", 32 | Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", 33 | }, 34 | []string{ 35 | "cluster", 36 | "subscribedTo", 37 | }) 38 | 39 | func IncActiveConnections(en *model.EnvoySubscriber) { 40 | METRICS_ACTIVE_CONNECTIONS.With(prometheus.Labels{ 41 | "cluster": en.Cluster, 42 | }).Inc() 43 | } 44 | 45 | func DecActiveConnections(en *model.EnvoySubscriber) { 46 | METRICS_ACTIVE_CONNECTIONS.With(prometheus.Labels{ 47 | "cluster": en.Cluster, 48 | }).Dec() 49 | } 50 | 51 | func IncActiveSubscribers(en *model.EnvoySubscriber, topic string) { 52 | METRICS_ACTIVE_SUBSCRIBERS.With(prometheus.Labels{ 53 | "cluster": en.Cluster, 54 | "type": topic, 55 | }).Inc() 56 | } 57 | 58 | func DecActiveSubscribers(en *model.EnvoySubscriber) { 59 | if en.IsADS() { 60 | for topic := range en.AdsList { 61 | METRICS_ACTIVE_SUBSCRIBERS.With(prometheus.Labels{ 62 | "cluster": en.Cluster, 63 | "type": topic, 64 | }).Dec() 65 | } 66 | } else { 67 | METRICS_ACTIVE_SUBSCRIBERS.With(prometheus.Labels{ 68 | "cluster": en.Cluster, 69 | "type": en.SubscribedTo, 70 | }).Dec() 71 | } 72 | } 73 | 74 | func IncXdsUpdateCounter(en *model.EnvoySubscriber) { 75 | XDS_UPDATE_COUNTER.With(prometheus.Labels{ 76 | "cluster": en.Cluster, 77 | "subscribedTo": en.SubscribedTo, 78 | }).Inc() 79 | } 80 | 81 | func StartMetricsServer() { 82 | prometheus.MustRegister(METRICS_ACTIVE_CONNECTIONS) 83 | prometheus.MustRegister(METRICS_ACTIVE_SUBSCRIBERS) 84 | prometheus.MustRegister(XDS_UPDATE_COUNTER) 85 | 86 | http.Handle("/metrics", promhttp.Handler()) 87 | 88 | log.Println("Starting metrics server on :8081..") 89 | err := http.ListenAndServe(":8081", nil) 90 | if err != nil { 91 | log.Fatal("ListenAndServe: ", err) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /cmd/server/model/config_meta.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ConfigMeta struct { 4 | Key string 5 | Topic string 6 | Version string 7 | } 8 | -------------------------------------------------------------------------------- /cmd/server/model/envoy_subscriber.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "Envoy-Pilot/cmd/server/util" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "time" 10 | ) 11 | 12 | type EnvoySubscriber struct { 13 | Guid string 14 | Cluster string 15 | Node string 16 | UpdateSuccess int 17 | UpdateFailures int 18 | LastUpdatedVersion string 19 | // LastUpdatedVersionV2 map[string]string // for ads 20 | LastUpdatedTimestamp time.Time 21 | SubscribedTo string 22 | // SubscribedToV2 []string // for ads 23 | AdsList map[string]*EnvoySubscriber 24 | IpAddress string 25 | } 26 | 27 | func (e *EnvoySubscriber) ToJSON() string { 28 | json, err := json.Marshal(e) 29 | if err != nil { 30 | log.Println("Error converting envoySubscriber to json..") 31 | panic(err) 32 | } 33 | return string(json) 34 | } 35 | 36 | func (e *EnvoySubscriber) BuildInstanceKey2() string { 37 | // return fmt.Sprintf("cluster/%s/node/%s/%s/%d", e.Cluster, e.Node, e.SubscribedTo, e.Id) 38 | if constant.FILE_MODE { 39 | return fmt.Sprintf("%s/%s/%s", e.Cluster, e.SubscribedTo, e.Guid) 40 | } 41 | return fmt.Sprintf("%s/app-cluster/%s/%s/%s", constant.CONSUL_PREFIX, e.Cluster, e.SubscribedTo, e.Guid) 42 | } 43 | 44 | func (e *EnvoySubscriber) BuildRootKey() string { 45 | // return fmt.Sprintf("cluster/%s/node/%s/%s/", e.Cluster, e.Node, e.SubscribedTo) 46 | if constant.FILE_MODE { 47 | return fmt.Sprintf("%s/%s/%s/", constant.FOLDER_PATH, e.Cluster, e.SubscribedTo) 48 | } 49 | return fmt.Sprintf("%s/app-cluster/%s/%s/", constant.CONSUL_PREFIX, e.Cluster, e.SubscribedTo) 50 | } 51 | 52 | func (e *EnvoySubscriber) IsEqual(that *EnvoySubscriber) bool { 53 | return e.Cluster == that.Cluster && e.Node == that.Node && e.UpdateSuccess == that.UpdateSuccess && e.UpdateFailures == that.UpdateFailures && e.LastUpdatedVersion == that.LastUpdatedVersion 54 | } 55 | 56 | func (e *EnvoySubscriber) IsADS() bool { 57 | return e.SubscribedTo == constant.SUBSCRIBE_ADS 58 | } 59 | 60 | func (e *EnvoySubscriber) GetAdsSubscriber(topic string) *EnvoySubscriber { 61 | return e.AdsList[topic] 62 | } 63 | 64 | func (e *EnvoySubscriber) IsOutdated(newVersion string) bool { 65 | latest := util.TrimVersion(newVersion) 66 | actual := util.TrimVersion(e.LastUpdatedVersion) 67 | return latest != actual 68 | } 69 | -------------------------------------------------------------------------------- /cmd/server/server/adsImpl.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "Envoy-Pilot/cmd/server/metrics" 6 | "Envoy-Pilot/cmd/server/model" 7 | "Envoy-Pilot/cmd/server/util" 8 | "context" 9 | "errors" 10 | "log" 11 | 12 | "google.golang.org/grpc/peer" 13 | 14 | discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2" 15 | ) 16 | 17 | //IncrementalAggregatedResources - Not implemented 18 | func (s *Server) IncrementalAggregatedResources(_ discovery.AggregatedDiscoveryService_IncrementalAggregatedResourcesServer) error { 19 | panic("Not implemented") 20 | } 21 | 22 | // StreamAggregatedResources - ADS server impl 23 | func (s *Server) StreamAggregatedResources(stream discovery.AggregatedDiscoveryService_StreamAggregatedResourcesServer) error { 24 | clientPeer, ok := peer.FromContext(stream.Context()) 25 | clientIP := "unknown" 26 | if ok { 27 | clientIP = clientPeer.Addr.String() 28 | } 29 | log.Printf("[%s] -------------- Starting a %s stream from %s ------------------\n", constant.SUBSCRIBE_ADS, constant.SUBSCRIBE_ADS, clientIP) 30 | 31 | serverCtx, cancel := context.WithCancel(context.Background()) 32 | 33 | dispatchChannel := make(chan model.ConfigMeta) 34 | i := 0 35 | var subscriber *model.EnvoySubscriber 36 | 37 | for { 38 | req, err := stream.Recv() 39 | if err != nil { 40 | log.Printf("[%s] Disconnecting client %s\n", constant.SUBSCRIBE_ADS, subscriber.BuildInstanceKey2()) 41 | log.Println(err) 42 | cancel() 43 | registerService.DeleteSubscriber(subscriber) 44 | metrics.DecActiveConnections(subscriber) 45 | metrics.DecActiveSubscribers(subscriber) 46 | return err 47 | } 48 | if i == 0 { 49 | if !IsValidSubscriber(req) { 50 | log.Printf("[%s] Error: Invalid cluster or node id %+v\n", constant.SUBSCRIBE_ADS, req) 51 | cancel() 52 | return errors.New("Invalid cluster or node id") 53 | } 54 | subscriber = &model.EnvoySubscriber{ 55 | Cluster: req.Node.Cluster, 56 | Node: req.Node.Id, 57 | SubscribedTo: constant.SUBSCRIBE_ADS, 58 | LastUpdatedVersion: util.TrimVersion(req.VersionInfo), 59 | AdsList: make(map[string]*model.EnvoySubscriber), 60 | IpAddress: clientIP, 61 | } 62 | serverCtx = context.WithValue(serverCtx, envoySubscriberKey, subscriber) 63 | registerService.RegisterEnvoy(serverCtx, stream, subscriber, dispatchChannel) 64 | metrics.IncActiveConnections(subscriber) 65 | i++ 66 | } 67 | 68 | topic := v2Helper.GetTopicFor(req.TypeUrl) 69 | var currentSubscriber *model.EnvoySubscriber 70 | 71 | if subscriber.AdsList[topic] == nil { 72 | currentSubscriber = &model.EnvoySubscriber{ 73 | Cluster: req.Node.Cluster, 74 | Node: req.Node.Id, 75 | SubscribedTo: topic, 76 | LastUpdatedVersion: util.TrimVersion(req.VersionInfo), 77 | IpAddress: clientIP, 78 | } 79 | subscriber.AdsList[topic] = currentSubscriber 80 | registerService.RegisterEnvoyADS(serverCtx, stream, currentSubscriber, dispatchChannel) 81 | metrics.IncActiveSubscribers(subscriber, currentSubscriber.SubscribedTo) 82 | } else { 83 | currentSubscriber = subscriber.AdsList[topic] 84 | } 85 | 86 | log.Printf("[%s] Received Request from %s\n %s\n", constant.SUBSCRIBE_ADS, currentSubscriber.BuildInstanceKey2(), util.ToJson(req)) 87 | 88 | if subscriberDao.IsACK(currentSubscriber, req.ResponseNonce) { 89 | dispatchService.HandleACK(currentSubscriber, req) 90 | continue 91 | } else { 92 | log.Printf("[%s] Response nonce not recognized %s", constant.SUBSCRIBE_ADS, req.ResponseNonce) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /cmd/server/server/clusterImpl.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "context" 6 | 7 | "errors" 8 | "log" 9 | 10 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 11 | ) 12 | 13 | func (s *Server) FetchClusters(ctx context.Context, in *v2.DiscoveryRequest) (*v2.DiscoveryResponse, error) { 14 | log.Printf("%+v\n", in) 15 | panic("Not implemented") 16 | return &v2.DiscoveryResponse{VersionInfo: "2"}, nil 17 | } 18 | 19 | func (s *Server) IncrementalClusters(_ v2.ClusterDiscoveryService_IncrementalClustersServer) error { 20 | return errors.New("not implemented") 21 | } 22 | 23 | // StreamClusters bi directional stream to update cluster config 24 | func (s *Server) StreamClusters(stream v2.ClusterDiscoveryService_StreamClustersServer) error { 25 | return s.BiDiStreamFor(constant.SUBSCRIBE_CDS, stream) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/server/server/endpointImpl.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "context" 6 | 7 | "log" 8 | 9 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 10 | ) 11 | 12 | func (s *Server) FetchEndpoints(ctx context.Context, in *v2.DiscoveryRequest) (*v2.DiscoveryResponse, error) { 13 | log.Printf("%+v\n", in) 14 | panic("Not implemented") 15 | return &v2.DiscoveryResponse{VersionInfo: "2"}, nil 16 | } 17 | 18 | // func (s *Server) IncrementalEndpoints(_ v2.ClusterDiscoveryService_IncrementalClustersServer) error { 19 | // return errors.New("not implemented") 20 | // } 21 | 22 | // StreamClusters bi directional stream to update endpoints config 23 | func (s *Server) StreamEndpoints(stream v2.EndpointDiscoveryService_StreamEndpointsServer) error { 24 | return s.BiDiStreamFor(constant.SUBSCRIBE_EDS, stream) 25 | } 26 | -------------------------------------------------------------------------------- /cmd/server/server/listenerImpl.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "context" 6 | 7 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 8 | ) 9 | 10 | func (s *Server) FetchListeners(context.Context, *v2.DiscoveryRequest) (*v2.DiscoveryResponse, error) { 11 | panic("Not implemented") 12 | } 13 | 14 | func (s *Server) StreamListeners(stream v2.ListenerDiscoveryService_StreamListenersServer) error { 15 | return s.BiDiStreamFor(constant.SUBSCRIBE_LDS, stream) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/server/server/routeImpl.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "context" 6 | 7 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 8 | ) 9 | 10 | func (s *Server) FetchRoutes(context.Context, *v2.DiscoveryRequest) (*v2.DiscoveryResponse, error) { 11 | panic("Not implemented") 12 | } 13 | 14 | func (s *Server) IncrementalRoutes(v2.RouteDiscoveryService_IncrementalRoutesServer) error { 15 | panic("Not implemented") 16 | } 17 | 18 | func (s *Server) StreamRoutes(stream v2.RouteDiscoveryService_StreamRoutesServer) error { 19 | return s.BiDiStreamFor(constant.SUBSCRIBE_RDS, stream) 20 | } 21 | -------------------------------------------------------------------------------- /cmd/server/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/metrics" 5 | "Envoy-Pilot/cmd/server/model" 6 | "Envoy-Pilot/cmd/server/service" 7 | "Envoy-Pilot/cmd/server/storage" 8 | "Envoy-Pilot/cmd/server/util" 9 | "context" 10 | "errors" 11 | "log" 12 | 13 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 14 | "google.golang.org/grpc/peer" 15 | ) 16 | 17 | const envoySubscriberKey = "envoySubscriber" 18 | 19 | var registerService *service.RegisterService 20 | var dispatchService *service.DispatchService 21 | var xdsConfigDao storage.XdsConfigDao 22 | var subscriberDao *storage.SubscriberDao 23 | var v2Helper *service.V2HelperService 24 | 25 | func InitServerDeps() { 26 | registerService = service.GetRegisterService() 27 | dispatchService = service.GetDispatchService() 28 | xdsConfigDao = storage.GetXdsConfigDao() 29 | subscriberDao = storage.GetSubscriberDao() 30 | } 31 | 32 | // Server struct will impl CDS, LDS, RDS & ADS 33 | type Server struct{} 34 | 35 | // BiDiStreamFor common bi-directional stream impl for cds,lds,rds 36 | func (s *Server) BiDiStreamFor(xdsType string, stream service.XDSStreamServer) error { 37 | clientPeer, ok := peer.FromContext(stream.Context()) 38 | clientIP := "unknown" 39 | if ok { 40 | clientIP = clientPeer.Addr.String() 41 | } 42 | log.Printf("[%s] -------------- Starting a %s stream from %s ------------------\n", xdsType, xdsType, clientIP) 43 | 44 | serverCtx, cancel := context.WithCancel(context.Background()) 45 | dispatchChannel := make(chan model.ConfigMeta) 46 | i := 0 47 | var subscriber *model.EnvoySubscriber 48 | 49 | for { 50 | req, err := stream.Recv() 51 | if err != nil { 52 | log.Printf("[%s] Disconnecting client %s\n", xdsType, subscriber.BuildInstanceKey2()) 53 | log.Println(err) 54 | cancel() 55 | registerService.DeleteSubscriber(subscriber) 56 | metrics.DecActiveConnections(subscriber) 57 | metrics.DecActiveSubscribers(subscriber) 58 | return err 59 | } 60 | if i == 0 { 61 | if !IsValidSubscriber(req) { 62 | log.Printf("[%s] Error: Invalid cluster or node id %+v\n", xdsType, req) 63 | cancel() 64 | return errors.New("Invalid cluster or node id") 65 | } 66 | subscriber = &model.EnvoySubscriber{ 67 | Cluster: req.Node.Cluster, 68 | Node: req.Node.Id, 69 | SubscribedTo: xdsType, 70 | LastUpdatedVersion: util.TrimVersion(req.VersionInfo), 71 | IpAddress: clientIP, 72 | } 73 | serverCtx = context.WithValue(serverCtx, envoySubscriberKey, subscriber) 74 | registerService.RegisterEnvoy(serverCtx, stream, subscriber, dispatchChannel) 75 | metrics.IncActiveConnections(subscriber) 76 | metrics.IncActiveSubscribers(subscriber, subscriber.SubscribedTo) 77 | i++ 78 | } 79 | 80 | log.Printf("[%s] Received Request from %s\n %s\n", xdsType, subscriber.BuildInstanceKey2(), util.ToJson(req)) 81 | 82 | if subscriberDao.IsACK(subscriber, req.ResponseNonce) { 83 | dispatchService.HandleACK(subscriber, req) 84 | continue 85 | } else { 86 | log.Printf("[%s] Response nonce not recognized %s", xdsType, req.ResponseNonce) 87 | } 88 | } 89 | } 90 | 91 | func IsValidSubscriber(req *v2.DiscoveryRequest) bool { 92 | return (len(req.Node.Cluster) > 0) && (len(req.Node.Id) > 0) 93 | } 94 | -------------------------------------------------------------------------------- /cmd/server/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 7 | envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 8 | ) 9 | 10 | func TestServer_IsValidSubscriber(t *testing.T) { 11 | validReq := v2.DiscoveryRequest{ 12 | Node: &envoy_api_v2_core.Node{ 13 | Id: "replica-1", 14 | Cluster: "rider-service", 15 | }, 16 | } 17 | inValidReq1 := v2.DiscoveryRequest{ 18 | Node: &envoy_api_v2_core.Node{ 19 | Cluster: "rider-service", 20 | }, 21 | } 22 | inValidReq2 := v2.DiscoveryRequest{ 23 | Node: &envoy_api_v2_core.Node{ 24 | Id: "replica-1", 25 | }, 26 | } 27 | 28 | if !IsValidSubscriber(&validReq) { 29 | t.Errorf("%+v \n is valid subscriber", validReq) 30 | } 31 | 32 | if IsValidSubscriber(&inValidReq1) { 33 | t.Errorf("%+v \n is NOT valid subscriber", inValidReq1) 34 | } 35 | 36 | if IsValidSubscriber(&inValidReq2) { 37 | t.Errorf("%+v \n is NOT valid subscriber", inValidReq2) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /cmd/server/service/dispatchService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "Envoy-Pilot/cmd/server/mapper" 6 | "Envoy-Pilot/cmd/server/model" 7 | "Envoy-Pilot/cmd/server/storage" 8 | "Envoy-Pilot/cmd/server/util" 9 | "context" 10 | "log" 11 | 12 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | var singletonDispatchService *DispatchService 17 | 18 | type DispatchService struct { 19 | xdsConfigDao storage.XdsConfigDao 20 | subscriberDao *storage.SubscriberDao 21 | clusterMapper mapper.ClusterMapper 22 | listenerMapper mapper.ListenerMapper 23 | v2HelperService *V2HelperService 24 | } 25 | 26 | func GetDispatchService() *DispatchService { 27 | if singletonDispatchService == nil { 28 | singletonDispatchService = &DispatchService{ 29 | xdsConfigDao: storage.GetXdsConfigDao(), 30 | subscriberDao: storage.GetSubscriberDao(), 31 | clusterMapper: mapper.ClusterMapper{}, 32 | v2HelperService: &V2HelperService{}, 33 | } 34 | } 35 | return singletonDispatchService 36 | } 37 | 38 | // XDSStreamServer common data type for xDS stream 39 | type XDSStreamServer interface { 40 | Send(*v2.DiscoveryResponse) error 41 | Recv() (*v2.DiscoveryRequest, error) 42 | Context() context.Context 43 | } 44 | 45 | func (c *DispatchService) buildDiscoveryResponseFor(subscriber *model.EnvoySubscriber) (*v2.DiscoveryResponse, error) { 46 | mapper := mapper.GetMapperFor(subscriber.SubscribedTo) 47 | configJson, version := c.xdsConfigDao.GetConfigJson(subscriber) 48 | clusterObj, err := mapper.GetResources(configJson) 49 | 50 | if err != nil { 51 | log.Printf("Unable to build discovery response for %s\n", subscriber.BuildInstanceKey2()) 52 | log.Println(err) 53 | return nil, err 54 | } 55 | 56 | responseUUID := uuid.New().String() 57 | response := &v2.DiscoveryResponse{ 58 | VersionInfo: version, 59 | Resources: clusterObj, 60 | TypeUrl: c.v2HelperService.GetTypeUrlFor(subscriber.SubscribedTo), 61 | Nonce: responseUUID, 62 | } 63 | return response, nil 64 | } 65 | 66 | func (c *DispatchService) dispatchData(ctx context.Context, stream XDSStreamServer, 67 | dispatchChannel chan model.ConfigMeta) { 68 | for updateInfo := range dispatchChannel { 69 | select { 70 | case <-ctx.Done(): 71 | return 72 | default: 73 | } 74 | 75 | subscriber := ctx.Value(constant.ENVOY_SUBSCRIBER_KEY).(*model.EnvoySubscriber) 76 | // var currentSubscriber *model.EnvoySubscriber 77 | if subscriber.IsADS() { 78 | subscriber = subscriber.GetAdsSubscriber(updateInfo.Topic) 79 | util.CheckNil(subscriber) 80 | } 81 | response, err := c.buildDiscoveryResponseFor(subscriber) 82 | if err != nil { 83 | log.Panicf("Unable to dispatch for %s\n", subscriber.BuildInstanceKey2()) 84 | continue 85 | } 86 | 87 | // TODO add log level 88 | // log.Printf("%+v\n", response) 89 | // log.Printf("Sending config to %s \n %+v \n", subscriber.BuildInstanceKey2(), response) 90 | 91 | c.subscriberDao.SaveNonce(subscriber, response.Nonce) 92 | err = stream.Send(response) 93 | if err != nil { 94 | log.Println("error sending to client") 95 | log.Println(err) 96 | c.subscriberDao.RemoveNonce(subscriber, response.Nonce) 97 | } else { 98 | log.Printf("Successfully Sent config to %s \n", subscriber.BuildInstanceKey2()) 99 | } 100 | } 101 | } 102 | 103 | // HandleACK check if the response is an ACK 104 | // if not ignore 105 | // if yes update the last updated version 106 | func (c *DispatchService) HandleACK(subscriber *model.EnvoySubscriber, req *v2.DiscoveryRequest) { 107 | log.Printf("Received ACK %s from %s", req.ResponseNonce, subscriber.BuildInstanceKey2()) 108 | c.subscriberDao.RemoveNonce(subscriber, req.ResponseNonce) 109 | subscriber.LastUpdatedVersion = req.VersionInfo 110 | } 111 | -------------------------------------------------------------------------------- /cmd/server/service/registerService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/model" 5 | "Envoy-Pilot/cmd/server/storage" 6 | "context" 7 | "log" 8 | 9 | "github.com/rs/xid" 10 | ) 11 | 12 | var singletonRegisterService *RegisterService 13 | var pollTopics = make(map[string]*model.ConfigMeta) 14 | 15 | // RegisterService a service class for cluster specific functionalities 16 | type RegisterService struct { 17 | xdsConfigDao storage.XdsConfigDao 18 | subscriberDao *storage.SubscriberDao 19 | dispatchService *DispatchService 20 | watchService *WatchService 21 | } 22 | 23 | // GetRegisterService get a singleton instance 24 | func GetRegisterService() *RegisterService { 25 | if singletonRegisterService == nil { 26 | singletonRegisterService = &RegisterService{ 27 | xdsConfigDao: storage.GetXdsConfigDao(), 28 | subscriberDao: storage.GetSubscriberDao(), 29 | dispatchService: GetDispatchService(), 30 | watchService: GetWatchService(), 31 | } 32 | } 33 | return singletonRegisterService 34 | } 35 | 36 | func GetPollTopics() map[string]*model.ConfigMeta { 37 | return pollTopics 38 | } 39 | 40 | // RegisterEnvoy register & subscribe new envoy instance 41 | func (c *RegisterService) RegisterEnvoy(ctx context.Context, 42 | stream XDSStreamServer, 43 | subscriber *model.EnvoySubscriber, dispatchChannel chan model.ConfigMeta) { 44 | if subscriber.IsADS() { 45 | c.subscriberDao.RegisterSubscriber(subscriber) 46 | go c.watchService.listenForUpdatesADS(ctx, dispatchChannel) 47 | go c.dispatchService.dispatchData(ctx, stream, dispatchChannel) 48 | } else { 49 | c.subscriberDao.RegisterSubscriber(subscriber) 50 | go c.watchService.listenForUpdates(ctx, dispatchChannel) 51 | go c.dispatchService.dispatchData(ctx, stream, dispatchChannel) 52 | } 53 | } 54 | 55 | func (c *RegisterService) RegisterEnvoyADS(ctx context.Context, 56 | stream XDSStreamServer, 57 | subscriber *model.EnvoySubscriber, dispatchChannel chan model.ConfigMeta) { 58 | subscriber.Guid = xid.New().String() 59 | c.watchService.register(subscriber) 60 | c.watchService.firstTimeCheck(subscriber, dispatchChannel) 61 | } 62 | 63 | // RemoveSubscriber Delete entry 64 | func (c *RegisterService) DeleteSubscriber(subscriber *model.EnvoySubscriber) { 65 | c.subscriberDao.DeleteSubscriber(subscriber) 66 | log.Printf("Deleting subscriber %s\n", subscriber.BuildInstanceKey2()) 67 | // TODO delete polling topic only if subcsribers are empty 68 | 69 | // if subscriber.IsADS() { 70 | // for _, topic := range constant.SUPPORTED_TYPES { 71 | // sub := subscriber.AdsList[topic] 72 | // if sub != nil { 73 | // delete(pollTopics, sub.BuildRootKey()) 74 | // } 75 | // } 76 | // } else { 77 | // delete(pollTopics, subscriber.BuildRootKey()) 78 | // } 79 | } 80 | -------------------------------------------------------------------------------- /cmd/server/service/v2HelperService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "fmt" 6 | 7 | "github.com/envoyproxy/go-control-plane/pkg/cache" 8 | ) 9 | 10 | type V2HelperService struct{} 11 | 12 | func (v *V2HelperService) GetTypeUrlFor(topic string) string { 13 | switch topic { 14 | case constant.SUBSCRIBE_CDS: 15 | return cache.ClusterType 16 | case constant.SUBSCRIBE_LDS: 17 | return cache.ListenerType 18 | case constant.SUBSCRIBE_RDS: 19 | return cache.RouteType 20 | case constant.SUBSCRIBE_EDS: 21 | return cache.EndpointType 22 | default: 23 | panic(fmt.Sprintf("No TypeUrl found for type %s\n", topic)) 24 | } 25 | } 26 | 27 | func (v *V2HelperService) GetTopicFor(typeUrl string) string { 28 | switch typeUrl { 29 | case cache.ClusterType: 30 | return constant.SUBSCRIBE_CDS 31 | case cache.ListenerType: 32 | return constant.SUBSCRIBE_LDS 33 | case cache.RouteType: 34 | return constant.SUBSCRIBE_RDS 35 | case cache.EndpointType: 36 | return constant.SUBSCRIBE_EDS 37 | default: 38 | panic(fmt.Sprintf("No Topic found for typeUrl %s\n", typeUrl)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cmd/server/service/watchService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "Envoy-Pilot/cmd/server/metrics" 6 | "Envoy-Pilot/cmd/server/model" 7 | "Envoy-Pilot/cmd/server/storage" 8 | "Envoy-Pilot/cmd/server/util" 9 | "context" 10 | "log" 11 | "time" 12 | ) 13 | 14 | var singletonWatchService *WatchService 15 | var versionChangeChannel = make(chan model.ConfigMeta) 16 | 17 | type WatchService struct { 18 | xdsConfigDao storage.XdsConfigDao 19 | subscriberDao *storage.SubscriberDao 20 | dispatchService *DispatchService 21 | } 22 | 23 | // WatchService get a singleton instance 24 | func GetWatchService() *WatchService { 25 | if singletonWatchService == nil { 26 | singletonWatchService = &WatchService{ 27 | xdsConfigDao: storage.GetXdsConfigDao(), 28 | subscriberDao: storage.GetSubscriberDao(), 29 | dispatchService: GetDispatchService(), 30 | } 31 | } 32 | return singletonWatchService 33 | } 34 | 35 | func (c *WatchService) firstTimeCheck(subscriber *model.EnvoySubscriber, dispatchChannel chan model.ConfigMeta) { 36 | if !c.xdsConfigDao.IsRepoPresent(subscriber) { 37 | log.Printf("No repo found for subscriber %s\n", subscriber.BuildRootKey()) 38 | } else { 39 | latestVersion := c.xdsConfigDao.GetLatestVersion(subscriber) 40 | if subscriber.IsOutdated(latestVersion) { 41 | log.Printf("Found update %s --> %s dispatching for %s\n", subscriber.LastUpdatedVersion, latestVersion, subscriber.BuildInstanceKey2()) 42 | dispatchChannel <- model.ConfigMeta{Key: subscriber.BuildRootKey(), Topic: subscriber.SubscribedTo, Version: latestVersion} 43 | metrics.IncXdsUpdateCounter(subscriber) 44 | } else { 45 | log.Printf("Already Upto date %s[%s:%s]\n", subscriber.BuildInstanceKey2(), subscriber.LastUpdatedVersion, latestVersion) 46 | } 47 | } 48 | } 49 | 50 | func (c *WatchService) listenForUpdates(ctx context.Context, dispatchChannel chan model.ConfigMeta) { 51 | subscriber := ctx.Value(constant.ENVOY_SUBSCRIBER_KEY).(*model.EnvoySubscriber) 52 | util.CheckNil(subscriber) 53 | c.registerPollTopic(ctx) 54 | c.firstTimeCheck(subscriber, dispatchChannel) 55 | 56 | for message := range versionChangeChannel { 57 | select { 58 | case <-ctx.Done(): 59 | return 60 | default: 61 | } 62 | if message.Key == subscriber.BuildRootKey() { 63 | if subscriber.IsOutdated(message.Version) { 64 | log.Printf("Found update %s --> %s dispatching for %s\n", subscriber.LastUpdatedVersion, message.Version, subscriber.BuildInstanceKey2()) 65 | dispatchChannel <- message 66 | metrics.IncXdsUpdateCounter(subscriber) 67 | } 68 | } 69 | } 70 | } 71 | 72 | func (c *WatchService) listenForUpdatesADS(ctx context.Context, dispatchChannel chan model.ConfigMeta) { 73 | adsSubscriber := ctx.Value(constant.ENVOY_SUBSCRIBER_KEY).(*model.EnvoySubscriber) 74 | util.CheckNil(adsSubscriber) 75 | c.registerPollTopicADS(ctx) 76 | 77 | for _, subscriber := range adsSubscriber.AdsList { 78 | c.firstTimeCheck(subscriber, dispatchChannel) 79 | } 80 | 81 | for message := range versionChangeChannel { 82 | select { 83 | case <-ctx.Done(): 84 | return 85 | default: 86 | } 87 | adsSubscriber = ctx.Value(constant.ENVOY_SUBSCRIBER_KEY).(*model.EnvoySubscriber) 88 | for _, subscriber := range adsSubscriber.AdsList { 89 | if message.Key == subscriber.BuildRootKey() { 90 | if subscriber.IsOutdated(message.Version) && subscriber.SubscribedTo == message.Topic { 91 | log.Printf("Found update %s --> %s dispatching for %s\n", subscriber.LastUpdatedVersion, message.Version, subscriber.BuildInstanceKey2()) 92 | dispatchChannel <- message 93 | metrics.IncXdsUpdateCounter(subscriber) 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | func (c *WatchService) register(subscriber *model.EnvoySubscriber) { 101 | if _, ok := pollTopics[subscriber.BuildRootKey()]; !ok { 102 | pollTopics[subscriber.BuildRootKey()] = &model.ConfigMeta{Key: subscriber.BuildRootKey(), Topic: subscriber.SubscribedTo, Version: subscriber.LastUpdatedVersion} 103 | } 104 | } 105 | 106 | func (c *WatchService) registerPollTopic(ctx context.Context) { 107 | subscriber := ctx.Value(constant.ENVOY_SUBSCRIBER_KEY).(*model.EnvoySubscriber) 108 | c.register(subscriber) 109 | } 110 | 111 | func (c *WatchService) registerPollTopicADS(ctx context.Context) { 112 | adsSubscriber := ctx.Value(constant.ENVOY_SUBSCRIBER_KEY).(*model.EnvoySubscriber) 113 | for _, topic := range constant.SUPPORTED_TYPES { 114 | subscriber := adsSubscriber.AdsList[topic] 115 | if subscriber != nil { 116 | c.register(subscriber) 117 | } 118 | } 119 | } 120 | 121 | func ConsulPollLoop() { 122 | pushService := GetRegisterService() 123 | log.Printf("Starting poll loop for %s..\n", constant.POLL_INTERVAL.String()) 124 | for { 125 | time.Sleep(constant.POLL_INTERVAL) 126 | for configKey, configMeta := range pollTopics { 127 | latestVersion := pushService.xdsConfigDao.GetLatestVersionFor(configKey) 128 | if pushService.xdsConfigDao.IsRepoPresentFor(configKey) { 129 | meta := model.ConfigMeta{Key: configKey, Topic: configMeta.Topic, Version: latestVersion} 130 | log.Printf("[DEBUG] Dispatching Version %+v \n", meta) 131 | versionChangeChannel <- meta 132 | pollTopics[configKey] = &meta 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /cmd/server/storage/consul.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strconv" 9 | "sync" 10 | 11 | consul "github.com/hashicorp/consul/api" 12 | "github.com/joho/godotenv" 13 | ) 14 | 15 | var singletonConsulWrapper ConsulWrapper 16 | var CONSUL_PATH string 17 | var mux sync.Mutex 18 | 19 | const ( 20 | envoySubscriberSequenceKey = "envoySubscriberSequence" 21 | ) 22 | 23 | type ConsulWrapper struct { 24 | client *consul.Client 25 | } 26 | 27 | func GetConsulWrapper() ConsulWrapper { 28 | mux.Lock() 29 | defer mux.Unlock() 30 | if singletonConsulWrapper.client == nil { 31 | err := godotenv.Load(constant.ENV_PATH) 32 | if err != nil { 33 | log.Print(err) 34 | log.Fatal("Error loading .env file") 35 | } 36 | consulPath := os.Getenv("CONSUL_PATH") 37 | 38 | log.Printf("Consul Path: %s\n", consulPath) 39 | config := &consul.Config{Address: consulPath} 40 | 41 | singletonConsulWrapper = ConsulWrapper{} 42 | client, err := consul.NewClient(config) 43 | if err != nil { 44 | panic(err) 45 | } 46 | singletonConsulWrapper.client = client 47 | } 48 | return singletonConsulWrapper 49 | } 50 | 51 | // TODO add retry 52 | func (c *ConsulWrapper) GetUniqId() int { 53 | for i := 0; i < 100; i++ { 54 | res, id, err := c.checkAndSetUniqId() 55 | if err != nil { 56 | log.Println("Error updating uniq CAS") 57 | panic(err) 58 | } 59 | if res { 60 | return id 61 | } 62 | log.Println("Re generating uniq id") 63 | } 64 | 65 | panic("Unable to generate new id") 66 | } 67 | 68 | func GetSequenceKey() string { 69 | return fmt.Sprintf("%s/%s", constant.CONSUL_PREFIX, envoySubscriberSequenceKey) 70 | } 71 | 72 | func (c *ConsulWrapper) checkAndSetUniqId() (bool, int, error) { 73 | pair, _, err := c.client.KV().Get(GetSequenceKey(), nil) 74 | if err != nil { 75 | panic(err) 76 | } 77 | if pair == nil { 78 | log.Println("nil value...") 79 | c.Set(GetSequenceKey(), "1") 80 | return true, 1, nil 81 | // pair = c.Get(envoySubscriberSequenceKey) 82 | } 83 | id, err := strconv.Atoi(string(pair.Value)) 84 | if err != nil { 85 | log.Printf("Err getting uniq id: %s\n", pair.Value) 86 | panic(err) 87 | } 88 | 89 | log.Printf("Last id value is %d\n", id) 90 | newId := id + 1 91 | pair.Value = []byte(strconv.Itoa(newId)) 92 | res, _, err := c.client.KV().CAS(pair, nil) 93 | return res, newId, err 94 | } 95 | 96 | func (c *ConsulWrapper) Set(key string, value string) { 97 | p := &consul.KVPair{Key: key, Value: []byte(value)} 98 | _, err := c.client.KV().Put(p, nil) 99 | if err != nil { 100 | log.Println(err) 101 | panic(err) 102 | } 103 | } 104 | 105 | func (c *ConsulWrapper) Get(key string) *consul.KVPair { 106 | pair, _, err := c.client.KV().Get(key, nil) 107 | if err != nil { 108 | panic(err) 109 | } 110 | // if pair == nil { 111 | // log.Printf("Nil value for key %s\n", key) 112 | // } 113 | return pair 114 | } 115 | 116 | func (c *ConsulWrapper) GetString(key string) string { 117 | pair := c.Get(key) 118 | if pair != nil { 119 | return string(pair.Value) 120 | } else { 121 | return "" 122 | } 123 | } 124 | 125 | func (c *ConsulWrapper) GetInt(key string) int { 126 | pair := c.Get(key) 127 | id, err := strconv.Atoi(string(pair.Value)) 128 | if err != nil { 129 | log.Println("Err getting uniq id") 130 | panic(err) 131 | } 132 | return id 133 | } 134 | 135 | func (c *ConsulWrapper) Delete(key string) error { 136 | _, err := c.client.KV().Delete(key, nil) 137 | if err != nil { 138 | log.Printf("Error deleting key %s\n", key) 139 | return err 140 | } 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /cmd/server/storage/consulConfigDao.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/model" 5 | "Envoy-Pilot/cmd/server/util" 6 | ) 7 | 8 | var once *ConsulConfigDao 9 | 10 | type ConsulConfigDao struct { 11 | consulWrapper ConsulWrapper 12 | } 13 | 14 | func (dao *ConsulConfigDao) GetLatestVersion(sub *model.EnvoySubscriber) string { 15 | return util.TrimVersion(dao.consulWrapper.GetString(sub.BuildRootKey() + "version")) 16 | } 17 | 18 | func (dao *ConsulConfigDao) GetLatestVersionFor(subscriberKey string) string { 19 | return util.TrimVersion(dao.consulWrapper.GetString(subscriberKey + "version")) 20 | } 21 | 22 | func (dao *ConsulConfigDao) IsRepoPresent(sub *model.EnvoySubscriber) bool { 23 | if dao.consulWrapper.Get(sub.BuildRootKey()+"version") == nil || dao.consulWrapper.Get(sub.BuildRootKey()+"config") == nil { 24 | return false 25 | } 26 | return true 27 | } 28 | 29 | func (dao *ConsulConfigDao) IsRepoPresentFor(subscriberKey string) bool { 30 | if dao.consulWrapper.Get(subscriberKey+"version") == nil || dao.consulWrapper.Get(subscriberKey+"config") == nil { 31 | return false 32 | } 33 | return true 34 | } 35 | 36 | func (dao *ConsulConfigDao) GetConfigJson(sub *model.EnvoySubscriber) (string, string) { 37 | return dao.consulWrapper.GetString(sub.BuildRootKey() + "config"), dao.GetLatestVersion(sub) 38 | } 39 | 40 | func GetConsulConfigDao() *ConsulConfigDao { 41 | if once == nil { 42 | once = &ConsulConfigDao{consulWrapper: GetConsulWrapper()} 43 | } 44 | return once 45 | } 46 | -------------------------------------------------------------------------------- /cmd/server/storage/fileConfigDao.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "Envoy-Pilot/cmd/server/model" 6 | "Envoy-Pilot/cmd/server/util" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | 11 | "github.com/rs/xid" 12 | ) 13 | 14 | var onceFileConfigDao *FileConfigDao 15 | var onceVersion string 16 | 17 | type FileConfigDao struct { 18 | consulWrapper ConsulWrapper 19 | } 20 | 21 | func filePath(sub *model.EnvoySubscriber) string { 22 | return fmt.Sprintf("%s/%s/%s.yaml", constant.FOLDER_PATH, sub.Cluster, sub.SubscribedTo) 23 | } 24 | 25 | func (dao *FileConfigDao) GetLatestVersion(sub *model.EnvoySubscriber) string { 26 | return util.TrimVersion(GetFileConfigVersion()) 27 | } 28 | 29 | func (dao *FileConfigDao) GetLatestVersionFor(subscriberKey string) string { 30 | return util.TrimVersion(GetFileConfigVersion()) 31 | } 32 | 33 | func (dao *FileConfigDao) IsRepoPresent(sub *model.EnvoySubscriber) bool { 34 | if _, err := os.Stat(filePath(sub)); !os.IsNotExist(err) { 35 | return true 36 | } 37 | return false 38 | } 39 | 40 | func (dao *FileConfigDao) IsRepoPresentFor(subscriberKey string) bool { 41 | if _, err := os.Stat(subscriberKey); !os.IsNotExist(err) { 42 | return true 43 | } 44 | return false 45 | } 46 | 47 | func (dao *FileConfigDao) GetConfigJson(sub *model.EnvoySubscriber) (string, string) { 48 | 49 | dat, err := ioutil.ReadFile(filePath(sub)) 50 | util.Check(err) 51 | return string(dat), dao.GetLatestVersion(sub) 52 | } 53 | 54 | func GetFileConfigDao() *FileConfigDao { 55 | if onceFileConfigDao == nil { 56 | onceFileConfigDao = &FileConfigDao{consulWrapper: GetConsulWrapper()} 57 | } 58 | return onceFileConfigDao 59 | } 60 | 61 | func GetFileConfigVersion() string { 62 | if len(onceVersion) == 0 { 63 | onceVersion = xid.New().String() 64 | } 65 | return onceVersion 66 | } 67 | -------------------------------------------------------------------------------- /cmd/server/storage/subscriberDao.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/cache" 5 | "Envoy-Pilot/cmd/server/model" 6 | "Envoy-Pilot/cmd/server/util" 7 | "fmt" 8 | "log" 9 | 10 | "github.com/rs/xid" 11 | ) 12 | 13 | type SubscriberDao struct { 14 | consulWrapper ConsulWrapper 15 | } 16 | 17 | var onceSubscriberDao *SubscriberDao 18 | 19 | func GetSubscriberDao() *SubscriberDao { 20 | if onceSubscriberDao == nil { 21 | onceSubscriberDao = &SubscriberDao{consulWrapper: GetConsulWrapper()} 22 | } 23 | return onceSubscriberDao 24 | } 25 | 26 | func (dao *SubscriberDao) RegisterSubscriber(sub *model.EnvoySubscriber) { 27 | guid := xid.New().String() 28 | if util.SyncMapExists(&cache.SUBSCRIBER_CACHE, guid) { 29 | log.Printf("---%s---\n", sub.Guid) 30 | log.Fatal(fmt.Sprintf("Subscrber %+v registered already", sub)) 31 | } 32 | sub.Guid = guid 33 | log.Printf("Registering subscriber %+v \n", sub) 34 | util.SyncMapSet(&cache.SUBSCRIBER_CACHE, guid, sub) 35 | } 36 | 37 | func (dao *SubscriberDao) DeleteSubscriber(sub *model.EnvoySubscriber) { 38 | util.SyncMapDelete(&cache.SUBSCRIBER_CACHE, sub.Guid) 39 | } 40 | 41 | func (dao *SubscriberDao) SaveNonce(sub *model.EnvoySubscriber, nonce string) { 42 | log.Printf("Writing ACK %s\n", nonceStreamKey(sub, nonce)) 43 | util.SyncMapSet(&cache.NONCE_CACHE, nonceStreamKey(sub, nonce), true) 44 | } 45 | 46 | func (dao *SubscriberDao) IsACK(sub *model.EnvoySubscriber, ack string) bool { 47 | return util.SyncMapExists(&cache.NONCE_CACHE, nonceStreamKey(sub, ack)) 48 | } 49 | 50 | func (dao *SubscriberDao) RemoveNonce(sub *model.EnvoySubscriber, nonce string) { 51 | util.SyncMapDelete(&cache.NONCE_CACHE, nonceStreamKey(sub, nonce)) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/server/storage/xdsConfigDao.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "Envoy-Pilot/cmd/server/constant" 5 | "Envoy-Pilot/cmd/server/model" 6 | "fmt" 7 | ) 8 | 9 | type XdsConfigDao interface { 10 | GetLatestVersion(sub *model.EnvoySubscriber) string 11 | GetLatestVersionFor(subscriberKey string) string 12 | IsRepoPresent(sub *model.EnvoySubscriber) bool 13 | IsRepoPresentFor(subscriberKey string) bool 14 | GetConfigJson(sub *model.EnvoySubscriber) (string, string) 15 | } 16 | 17 | func nonceStreamKey(sub *model.EnvoySubscriber, nonce string) string { 18 | return fmt.Sprintf("%s/Nonce/Stream/%s", sub.BuildInstanceKey2(), nonce) 19 | } 20 | 21 | func GetXdsConfigDao() XdsConfigDao { 22 | if constant.FILE_MODE { 23 | return GetFileConfigDao() 24 | } 25 | return GetConsulConfigDao() 26 | } 27 | -------------------------------------------------------------------------------- /cmd/server/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "strings" 7 | "sync" 8 | 9 | // yaml "gopkg.in/yaml.v2" 10 | yaml "github.com/ghodss/yaml" 11 | ) 12 | 13 | func Check(err error) { 14 | if err != nil { 15 | log.Println("[Util] Error..", err) 16 | } 17 | } 18 | 19 | func CheckAndPanic(err error) { 20 | if err != nil { 21 | log.Println("[Util] Error..", err) 22 | panic(err) 23 | } 24 | } 25 | 26 | func TrimVersion(version string) string { 27 | if len(version) != 0 { 28 | return strings.Trim(version, `"'`) 29 | } 30 | return "" 31 | } 32 | 33 | func CheckNil(obj interface{}) { 34 | if obj == nil { 35 | log.Fatal("Object is nil") 36 | } 37 | } 38 | 39 | func ToJson(obj interface{}) []byte { 40 | res, _ := json.MarshalIndent(&obj, "", "\t") 41 | return res 42 | } 43 | 44 | func ImportJsonOrYaml(jsonStr string) []interface{} { 45 | var rawArr []interface{} 46 | jsErr := json.Unmarshal([]byte(jsonStr), &rawArr) 47 | if jsErr == nil { 48 | return rawArr 49 | } 50 | 51 | yamlErr := yaml.Unmarshal([]byte(jsonStr), &rawArr) 52 | if yamlErr == nil { 53 | return rawArr 54 | } 55 | 56 | panic("Invalid json or yaml..") 57 | } 58 | 59 | func SyncMapExists(m *sync.Map, k string) bool { 60 | _, res := m.Load(k) 61 | return res 62 | } 63 | 64 | func SyncMapGetString(m *sync.Map, k string) string { 65 | res, _ := m.Load(k) 66 | return res.(string) 67 | } 68 | 69 | func SyncMapSet(m *sync.Map, k string, v interface{}) { 70 | m.Store(k, v) 71 | } 72 | 73 | func SyncMapDelete(m *sync.Map, k string) { 74 | m.Delete(k) 75 | } 76 | -------------------------------------------------------------------------------- /docker-compose.consul.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | consul-agent-1: &consul-agent 5 | image: consul:latest 6 | networks: 7 | - envoy-pilot_xds-demo 8 | command: "agent -retry-join consul-server-bootstrap -client 0.0.0.0" 9 | 10 | consul-agent-2: 11 | <<: *consul-agent 12 | 13 | consul-agent-3: 14 | <<: *consul-agent 15 | 16 | consul-server-1: &consul-server 17 | <<: *consul-agent 18 | command: "agent -server -retry-join consul-server-bootstrap -client 0.0.0.0" 19 | 20 | consul-server-2: 21 | <<: *consul-server 22 | 23 | consul-server-bootstrap: 24 | <<: *consul-agent 25 | ports: 26 | - "8400:8400" 27 | - "8500:8500" 28 | - "8600:8600" 29 | - "8600:8600/udp" 30 | command: "agent -server -bootstrap-expect 3 -ui -client 0.0.0.0" 31 | expose: 32 | - "8500" 33 | 34 | networks: 35 | envoy-pilot_xds-demo: 36 | external: true -------------------------------------------------------------------------------- /docker-compose.server.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | xds-server: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile-xds-server 8 | # command: "sh -c 'while true; do sleep 1000; done'" 9 | # command: "sh -c 'dlv --api-version 2 debug Envoy-Pilot/cmd/server/ -l 0.0.0.0:2345 --headless=true --log=true -- server'" 10 | # command: "sh -c 'go run /go/src/Envoy-Pilot/cmd/server/main.go'" 11 | security_opt: 12 | - seccomp:unconfined 13 | volumes: 14 | - $PWD/env_values.txt:/.env 15 | expose: 16 | - 7777 17 | - 2345 18 | ports: 19 | - "2345:2345" 20 | - "9090:9090" 21 | - "7777:7777" 22 | - "8081:8081" 23 | networks: 24 | - envoy-pilot_xds-demo 25 | 26 | networks: 27 | envoy-pilot_xds-demo: 28 | external: true -------------------------------------------------------------------------------- /docker-compose.travis.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | xds-server: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile-xds-server 8 | security_opt: 9 | - seccomp:unconfined 10 | volumes: 11 | - $PWD/env_values.txt:/.env 12 | expose: 13 | - 7777 14 | - 2345 15 | ports: 16 | - "2345:2345" 17 | - "9090:9090" 18 | - "7777:7777" 19 | networks: 20 | - envoy-pilot_xds-demo 21 | 22 | networks: 23 | envoy-pilot_xds-demo: 24 | external: true -------------------------------------------------------------------------------- /env_values.txt: -------------------------------------------------------------------------------- 1 | CONSUL_PATH="http://consul-server-bootstrap:8500" 2 | CONSUL_PREFIX="xDS" 3 | FILE_MODE=false 4 | FOLDER_PATH="/file_mode_config/" 5 | POLL_INTERVAL="10s" -------------------------------------------------------------------------------- /lib/api.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api.proto 3 | 4 | package api 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | import ( 11 | context "golang.org/x/net/context" 12 | grpc "google.golang.org/grpc" 13 | ) 14 | 15 | // Reference imports to suppress errors if they are not otherwise used. 16 | var _ = proto.Marshal 17 | var _ = fmt.Errorf 18 | var _ = math.Inf 19 | 20 | // This is a compile-time assertion to ensure that this generated file 21 | // is compatible with the proto package it is being compiled against. 22 | // A compilation error at this line likely means your copy of the 23 | // proto package needs to be updated. 24 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 25 | 26 | type PingMessage struct { 27 | Greeting string `protobuf:"bytes,1,opt,name=greeting,proto3" json:"greeting,omitempty"` 28 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 29 | XXX_unrecognized []byte `json:"-"` 30 | XXX_sizecache int32 `json:"-"` 31 | } 32 | 33 | func (m *PingMessage) Reset() { *m = PingMessage{} } 34 | func (m *PingMessage) String() string { return proto.CompactTextString(m) } 35 | func (*PingMessage) ProtoMessage() {} 36 | func (*PingMessage) Descriptor() ([]byte, []int) { 37 | return fileDescriptor_api_914bc4555e9d8715, []int{0} 38 | } 39 | func (m *PingMessage) XXX_Unmarshal(b []byte) error { 40 | return xxx_messageInfo_PingMessage.Unmarshal(m, b) 41 | } 42 | func (m *PingMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 43 | return xxx_messageInfo_PingMessage.Marshal(b, m, deterministic) 44 | } 45 | func (dst *PingMessage) XXX_Merge(src proto.Message) { 46 | xxx_messageInfo_PingMessage.Merge(dst, src) 47 | } 48 | func (m *PingMessage) XXX_Size() int { 49 | return xxx_messageInfo_PingMessage.Size(m) 50 | } 51 | func (m *PingMessage) XXX_DiscardUnknown() { 52 | xxx_messageInfo_PingMessage.DiscardUnknown(m) 53 | } 54 | 55 | var xxx_messageInfo_PingMessage proto.InternalMessageInfo 56 | 57 | func (m *PingMessage) GetGreeting() string { 58 | if m != nil { 59 | return m.Greeting 60 | } 61 | return "" 62 | } 63 | 64 | func init() { 65 | proto.RegisterType((*PingMessage)(nil), "api.PingMessage") 66 | } 67 | 68 | // Reference imports to suppress errors if they are not otherwise used. 69 | var _ context.Context 70 | var _ grpc.ClientConn 71 | 72 | // This is a compile-time assertion to ensure that this generated file 73 | // is compatible with the grpc package it is being compiled against. 74 | const _ = grpc.SupportPackageIsVersion4 75 | 76 | // PingClient is the client API for Ping service. 77 | // 78 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 79 | type PingClient interface { 80 | SayHello(ctx context.Context, in *PingMessage, opts ...grpc.CallOption) (*PingMessage, error) 81 | } 82 | 83 | type pingClient struct { 84 | cc *grpc.ClientConn 85 | } 86 | 87 | func NewPingClient(cc *grpc.ClientConn) PingClient { 88 | return &pingClient{cc} 89 | } 90 | 91 | func (c *pingClient) SayHello(ctx context.Context, in *PingMessage, opts ...grpc.CallOption) (*PingMessage, error) { 92 | out := new(PingMessage) 93 | err := c.cc.Invoke(ctx, "/api.Ping/SayHello", in, out, opts...) 94 | if err != nil { 95 | return nil, err 96 | } 97 | return out, nil 98 | } 99 | 100 | // PingServer is the server API for Ping service. 101 | type PingServer interface { 102 | SayHello(context.Context, *PingMessage) (*PingMessage, error) 103 | } 104 | 105 | func RegisterPingServer(s *grpc.Server, srv PingServer) { 106 | s.RegisterService(&_Ping_serviceDesc, srv) 107 | } 108 | 109 | func _Ping_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 110 | in := new(PingMessage) 111 | if err := dec(in); err != nil { 112 | return nil, err 113 | } 114 | if interceptor == nil { 115 | return srv.(PingServer).SayHello(ctx, in) 116 | } 117 | info := &grpc.UnaryServerInfo{ 118 | Server: srv, 119 | FullMethod: "/api.Ping/SayHello", 120 | } 121 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 122 | return srv.(PingServer).SayHello(ctx, req.(*PingMessage)) 123 | } 124 | return interceptor(ctx, in, info, handler) 125 | } 126 | 127 | var _Ping_serviceDesc = grpc.ServiceDesc{ 128 | ServiceName: "api.Ping", 129 | HandlerType: (*PingServer)(nil), 130 | Methods: []grpc.MethodDesc{ 131 | { 132 | MethodName: "SayHello", 133 | Handler: _Ping_SayHello_Handler, 134 | }, 135 | }, 136 | Streams: []grpc.StreamDesc{}, 137 | Metadata: "api.proto", 138 | } 139 | 140 | func init() { proto.RegisterFile("api.proto", fileDescriptor_api_914bc4555e9d8715) } 141 | 142 | var fileDescriptor_api_914bc4555e9d8715 = []byte{ 143 | // 111 bytes of a gzipped FileDescriptorProto 144 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4c, 0x2c, 0xc8, 0xd4, 145 | 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x4e, 0x2c, 0xc8, 0x54, 0xd2, 0xe4, 0xe2, 0x0e, 0xc8, 146 | 0xcc, 0x4b, 0xf7, 0x4d, 0x2d, 0x2e, 0x4e, 0x4c, 0x4f, 0x15, 0x92, 0xe2, 0xe2, 0x48, 0x2f, 0x4a, 147 | 0x4d, 0x2d, 0xc9, 0xcc, 0x4b, 0x97, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x82, 0xf3, 0x8d, 0x2c, 148 | 0xb8, 0x58, 0x40, 0x4a, 0x85, 0x0c, 0xb8, 0x38, 0x82, 0x13, 0x2b, 0x3d, 0x52, 0x73, 0x72, 0xf2, 149 | 0x85, 0x04, 0xf4, 0x40, 0xe6, 0x21, 0x99, 0x20, 0x85, 0x21, 0xa2, 0xc4, 0x90, 0xc4, 0x06, 0xb6, 150 | 0xd0, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0xf8, 0x4a, 0xe3, 0x1e, 0x7d, 0x00, 0x00, 0x00, 151 | } 152 | -------------------------------------------------------------------------------- /snippet: -------------------------------------------------------------------------------- 1 | docker run --rm -it -v "$(pwd)"/test/envoy/config.yaml:/app/config.yaml envoyproxy/envoy:latest /bin/bash 2 | 3 | envoy -c /app/config.yaml -l debug --v2-config-only --service-cluster app-cluster --service-node app-node -------------------------------------------------------------------------------- /test/envoy/config.yaml: -------------------------------------------------------------------------------- 1 | admin: 2 | access_log_path: /tmp/admin_access.log 3 | address: 4 | socket_address: { address: 127.0.0.1, port_value: 9901 } 5 | 6 | dynamic_resources: 7 | cds_config: 8 | api_config_source: 9 | api_type: GRPC 10 | refresh_delay: 3s 11 | grpc_services: 12 | envoy_grpc: 13 | cluster_name: xds_cluster 14 | static_resources: 15 | listeners: 16 | - name: listener_0 17 | address: 18 | socket_address: { address: 127.0.0.1, port_value: 10000 } 19 | filter_chains: 20 | - filters: 21 | - name: envoy.http_connection_manager 22 | config: 23 | stat_prefix: ingress_http 24 | codec_type: AUTO 25 | route_config: 26 | name: local_route 27 | virtual_hosts: 28 | - name: local_service 29 | domains: ["*"] 30 | routes: 31 | - match: { prefix: "/" } 32 | route: { cluster: xds_cluster } 33 | http_filters: 34 | - name: envoy.router 35 | clusters: 36 | - name: xds_cluster 37 | connect_timeout: 0.25s 38 | type: strict_dns 39 | lb_policy: ROUND_ROBIN 40 | dns_refresh_rate: 500000000s 41 | http2_protocol_options: {} 42 | hosts: [{ socket_address: { address: host.docker.internal, port_value: 7777 }}] -------------------------------------------------------------------------------- /test/envoy/config_ads.yaml: -------------------------------------------------------------------------------- 1 | admin: 2 | access_log_path: /tmp/admin_access.log 3 | address: 4 | socket_address: { address: 127.0.0.1, port_value: 9901 } 5 | 6 | dynamic_resources: 7 | cds_config: {ads: {}} 8 | lds_config: {ads: {}} 9 | ads_config: 10 | api_type: GRPC 11 | grpc_services: 12 | envoy_grpc: 13 | cluster_name: ads_cluster 14 | static_resources: 15 | clusters: 16 | - name: ads_cluster 17 | connect_timeout: 0.25s 18 | type: strict_dns 19 | lb_policy: ROUND_ROBIN 20 | dns_refresh_rate: 500000000s 21 | http2_protocol_options: {} 22 | hosts: [{ socket_address: { address: host.docker.internal, port_value: 7777 }}] -------------------------------------------------------------------------------- /test/envoy/config_lds.yaml: -------------------------------------------------------------------------------- 1 | admin: 2 | access_log_path: /tmp/admin_access.log 3 | address: 4 | socket_address: { address: 127.0.0.1, port_value: 9901 } 5 | 6 | dynamic_resources: 7 | lds_config: 8 | api_config_source: 9 | api_type: GRPC 10 | grpc_services: 11 | envoy_grpc: 12 | cluster_name: xds_cluster 13 | cds_config: 14 | api_config_source: 15 | api_type: GRPC 16 | refresh_delay: 3s 17 | grpc_services: 18 | envoy_grpc: 19 | cluster_name: xds_cluster 20 | static_resources: 21 | clusters: 22 | - name: xds_cluster 23 | connect_timeout: 0.25s 24 | type: strict_dns 25 | lb_policy: ROUND_ROBIN 26 | dns_refresh_rate: 500000000s 27 | http2_protocol_options: {} 28 | hosts: [{ socket_address: { address: host.docker.internal, port_value: 7777 }}] -------------------------------------------------------------------------------- /test/integration/.env: -------------------------------------------------------------------------------- 1 | CONSUL_PATH="consul-server-bootstrap:8500" -------------------------------------------------------------------------------- /test/integration/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDXjCCAkYCCQClsmfHBWg53TANBgkqhkiG9w0BAQsFADBxMQswCQYDVQQGEwJp 3 | bjELMAkGA1UECAwCdG4xCzAJBgNVBAcMAmFkMQ0wCwYDVQQKDARhc2RmMRAwDgYD 4 | VQQLDAdhc2RmYXNkMQ4wDAYDVQQDDAVkZmFzZDEXMBUGCSqGSIb3DQEJARYIYXNk 5 | ZmFzZGYwHhcNMTgxMDMxMDc0OTE0WhcNMjgxMDI4MDc0OTE0WjBxMQswCQYDVQQG 6 | EwJpbjELMAkGA1UECAwCdG4xCzAJBgNVBAcMAmFkMQ0wCwYDVQQKDARhc2RmMRAw 7 | DgYDVQQLDAdhc2RmYXNkMQ4wDAYDVQQDDAVkZmFzZDEXMBUGCSqGSIb3DQEJARYI 8 | YXNkZmFzZGYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCk03rEhS0m 9 | 9AbLuludt3hDx/0TonuSIKvzP8d7L1A9pEVJDp9jiKob+GwSU41IH/cQDGGRVnPR 10 | 81l/FHCaNSYXmxJP5Mf7yxV/rYV5z8juBqpOvjYQ1GtEap+8S3bELiqNu5rxa9E+ 11 | RlRikfVk3PxbkaAdXNLty9XVWVxpZ5lPw/SZYcfv7gsNVHnkamB1y22F6ZiyMU/f 12 | 2sfnTl32JpzOqjdFmrci99KviSTFtUz6AGkxHt7KZBZpqLqYgr2zQ3AGmx1DdlEl 13 | gAa7MtgPrlWgMkip/Rs05Qlepc0FsjkjL8/b5PsJ22YXl9sNFwQsNCE96UpiJGww 14 | fw4MQzdewv3jAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAApEzbnas9VBncAIfrdt 15 | C6Px6979OstsG3PauYyrvzUbfjwMStU1SG16lttwaQoJVjU8emkui/lqLYqt0n+r 16 | cobPtwfIGaEQGhj8L+YsXU7JYxZq0MQ9uo/H+IK8zDMcXYc+0fsA1PXFsYRuZRX8 17 | i9iuytaZC5AuTC920UCzgWPfKzRTGnXHKz1r7tTiA4PaEEYTz2G18gVd59SLIsdt 18 | 4AlmQob8s57oKzvxKQpXPRMx7P10+F/3vNPqrMNdG0vRFt0tO68Aa2rm8Eloc+v1 19 | 2MRH821pmG4b0swm3PUPz+ubl2s7OR27Ksyw4Ha8so/n3TQKFw5mZLc6Nh+041ZO 20 | to8= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /test/integration/config-ads.yaml: -------------------------------------------------------------------------------- 1 | admin: 2 | access_log_path: /tmp/admin_access.log 3 | address: 4 | socket_address: { address: 0.0.0.0, port_value: 9901 } 5 | node: 6 | id: adstest-node 7 | cluster: adstest-cluster 8 | dynamic_resources: 9 | cds_config: {ads: {}} 10 | lds_config: {ads: {}} 11 | ads_config: 12 | api_type: GRPC 13 | grpc_services: 14 | envoy_grpc: 15 | cluster_name: xds_cluster 16 | static_resources: 17 | clusters: 18 | - name: xds_cluster 19 | connect_timeout: 0.25s 20 | type: strict_dns 21 | lb_policy: ROUND_ROBIN 22 | dns_refresh_rate: 500000000s 23 | http2_protocol_options: {} 24 | hosts: [{ socket_address: { address: "xds-server", port_value: 7777 }}] -------------------------------------------------------------------------------- /test/integration/config-cds-lds.yaml: -------------------------------------------------------------------------------- 1 | admin: 2 | access_log_path: /tmp/admin_access.log 3 | address: 4 | socket_address: { address: 0.0.0.0, port_value: 9901 } 5 | 6 | dynamic_resources: 7 | lds_config: 8 | api_config_source: 9 | api_type: GRPC 10 | grpc_services: 11 | envoy_grpc: 12 | cluster_name: xds_cluster 13 | cds_config: 14 | api_config_source: 15 | api_type: GRPC 16 | refresh_delay: 3s 17 | grpc_services: 18 | envoy_grpc: 19 | cluster_name: xds_cluster 20 | static_resources: 21 | clusters: 22 | - name: xds_cluster 23 | connect_timeout: 0.25s 24 | type: strict_dns 25 | lb_policy: ROUND_ROBIN 26 | dns_refresh_rate: 500000000s 27 | http2_protocol_options: {} 28 | hosts: [{ socket_address: { address: "xds-server", port_value: 7777 }}] -------------------------------------------------------------------------------- /test/integration/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app-server: 5 | build: 6 | context: dummy-app/ 7 | dockerfile: Dockerfile-dummy-app 8 | networks: 9 | - envoy-pilot_xds-demo 10 | ports: 11 | - "8123:8123" 12 | expose: 13 | - 8123 14 | envoy: 15 | image: envoyproxy/envoy:v1.7.0 16 | volumes: 17 | - $PWD/config-cds-lds.yaml:/config.yaml 18 | - $PWD/cert.pem:/etc/cert/cert.pem 19 | - $PWD/key.pem:/etc/pkey/pkey.pem 20 | command: ["envoy", "-c", "/config.yaml", "--v2-config-only", "-l", "debug", "--service-cluster","xdstest-cluster","--service-node","xdstest-node"] 21 | ports: 22 | - "9901:9901" 23 | - "18123:18123" 24 | networks: 25 | - envoy-pilot_xds-demo 26 | envoy-ads: 27 | image: envoyproxy/envoy:v1.7.0 28 | volumes: 29 | - $PWD/config-ads.yaml:/config.yaml 30 | - $PWD/cert.pem:/etc/cert/cert.pem 31 | - $PWD/key.pem:/etc/pkey/pkey.pem 32 | command: ["envoy", "-c", "/config.yaml", "--v2-config-only", "-l", "debug", "--service-cluster","adstest-cluster","--service-node","adstest-node"] 33 | ports: 34 | - "9902:9901" 35 | - "28123:18123" 36 | networks: 37 | - envoy-pilot_xds-demo 38 | 39 | networks: 40 | envoy-pilot_xds-demo: 41 | external: true -------------------------------------------------------------------------------- /test/integration/dummy-app/Dockerfile-dummy-app: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | 3 | RUN mkdir /go/src/server 4 | ADD main.go /go/src/server/main.go 5 | 6 | CMD ["go", "run", "/go/src/server/main.go"] -------------------------------------------------------------------------------- /test/integration/dummy-app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func handler(w http.ResponseWriter, r *http.Request) { 10 | fmt.Fprintf(w, "Responding to %s!", r.URL.Path[1:]) 11 | } 12 | 13 | func main() { 14 | http.HandleFunc("/abc", handler) 15 | log.Println("Started server :8123..") 16 | log.Fatal(http.ListenAndServe(":8123", nil)) 17 | } 18 | -------------------------------------------------------------------------------- /test/integration/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCk03rEhS0m9AbL 3 | uludt3hDx/0TonuSIKvzP8d7L1A9pEVJDp9jiKob+GwSU41IH/cQDGGRVnPR81l/ 4 | FHCaNSYXmxJP5Mf7yxV/rYV5z8juBqpOvjYQ1GtEap+8S3bELiqNu5rxa9E+RlRi 5 | kfVk3PxbkaAdXNLty9XVWVxpZ5lPw/SZYcfv7gsNVHnkamB1y22F6ZiyMU/f2sfn 6 | Tl32JpzOqjdFmrci99KviSTFtUz6AGkxHt7KZBZpqLqYgr2zQ3AGmx1DdlElgAa7 7 | MtgPrlWgMkip/Rs05Qlepc0FsjkjL8/b5PsJ22YXl9sNFwQsNCE96UpiJGwwfw4M 8 | Qzdewv3jAgMBAAECggEASSOScYvK+mMEdPiP2M9l7WXb1g+el7wpsPnXMcFP8Npo 9 | 6D812Pv2yAyXIBa1NZEBl7x2uwBKypoaV3qhcLwu2AC2wn75grCeLJa6CNB8D6sj 10 | fWeIJDWuF6Dcmrg9zlXSCix6V7CJHAInRcoQwx4QWS2oh/79lgmCLHbHu/n9nNfX 11 | pI4ApchrapLMvBvkkzbBPfxHkjXvPY/UlFwChJOH8m3kb7GXGpnfwsUPZW9VUbBl 12 | oHGW7ay8E/qmzwNhzM1XNqK6XwOejeWGK1uwoGzXJg6NScxk4SVPb4xA30UwnEiL 13 | S6V9zGbxSblD3VPxWJABoJSW3n08p8D4OhS/3kUbSQKBgQDZZwUbYdW1zNjoS6/a 14 | QMv1jAchDh2tFbLHY7va+qSUvf/8otH5D6auM40hKo2scWP4dXp1IliyVKWOoPNE 15 | vz0oj67EqGemKP8N2uViEz/7840QGhVJueUSvGdGype3Ee7hux/vGrtJ2qxq1w46 16 | aP8X0eBICaIBqoRYOFFub22LXwKBgQDCFtsTE6CMgiRxV9zS0Y5s4vmCOgODfZrR 17 | elUw6C0WNjzRWMtyYHU0iQp1ViIDT9fSlqiwDTqoCzMNmYHtIjHsniihBXxSZbCs 18 | z312uXipkADJZRBrqQUv8UoCMKmjF5KksRRCyVhhvZaXACukXyqzTlB6DFhPYkBT 19 | FY/uZDhf/QKBgQCqBa2yyoLOmZQRqA+xivd347k3msiOsueMlG04RsksIqPuuo+o 20 | Txs2Jc7730HJWSysBYRt0xy26whHUYyveTilXil6V5IoGuvNtCGs4A0sSD9MFnoL 21 | nLyQOJQ0gwDABeBi3WuOHcPXcJNjQyk3eSH3SQY0kIQI9YhnTjQxQCfV1QKBgHla 22 | sxXFcuAHy2N3DWJflo7siBdt0J6ZnYCW3cdblD1MIlC/FU3pk78KZJijB1dMx+Qr 23 | QUP6sY86mWxKbBtqAw8wgLTVajtWy9XxUkA2jYRvvp4t68t0/gJQ+vZNmPOZnJ4O 24 | /l0X6YQLd6noeGEpA9jjAZyeNWVFSHTqiXkD/t+9AoGBAJxaaKtM3C236KTujuWA 25 | CrGZGPlys1gYFN4skBfFnRKZIOaK3bQHKvJDdqTRdoulTdoYwbMOPr58R2ideZf+ 26 | 24xqBaic0E+CrSSK2M2vl37tz5sHuVsSPsnEShVKIDQYo8ACU4AwCfrBPX3npyV+ 27 | l8K2tJwc4x3Xuw58pDeVKzzI 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/rspec/basic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rest-client' 2 | require 'json' 3 | require 'plissken' 4 | require 'diplomat' 5 | 6 | Diplomat.configure do |config| 7 | config.url = 'http://localhost:8500' 8 | end 9 | 10 | listener0_json = File.read 'json/listener_0.json' 11 | listener1_json = File.read 'json/listener_1.json' 12 | listener2_json = File.read 'json/listener_2.json' 13 | listener3_json = File.read 'json/listener_3.json' 14 | 15 | listeners_json = %Q{ 16 | [ 17 | #{listener0_json}, 18 | #{listener1_json}, 19 | #{listener2_json}, 20 | #{listener3_json} 21 | ] 22 | } 23 | 24 | cluster0_json = File.read 'json/cluster_0.json' 25 | cluster1_json = File.read 'json/cluster_1.json' 26 | cluster2_json = File.read 'json/cluster_2.json' 27 | cluster3_json = File.read 'json/cluster_3.json' 28 | 29 | clusters_json = %Q{ 30 | [ 31 | #{cluster0_json}, 32 | #{cluster1_json}, 33 | #{cluster2_json}, 34 | #{cluster3_json} 35 | ] 36 | } 37 | 38 | route0_json = %Q{ 39 | { 40 | "name": "listener_1_route", 41 | "virtual_hosts": [ 42 | { 43 | "name": "local_service", 44 | "domains": [ 45 | "*" 46 | ], 47 | "routes": [ 48 | { 49 | "match": { 50 | "prefix": "/" 51 | }, 52 | "route": { 53 | "cluster": "app1" 54 | } 55 | } 56 | ] 57 | } 58 | ] 59 | } 60 | } 61 | 62 | routes_json = %Q{ 63 | [ 64 | #{route0_json} 65 | ] 66 | } 67 | 68 | endpoint0_json = %Q{ 69 | { 70 | "cluster_name": "some_service", 71 | "endpoints": [ 72 | { 73 | "lb_endpoints": [ 74 | { 75 | "endpoint": { 76 | "address": { 77 | "socket_address": { 78 | "address": "app-server", 79 | "port_value": 8123 80 | } 81 | } 82 | } 83 | } 84 | ] 85 | } 86 | ] 87 | } 88 | } 89 | 90 | endpoints_json = %Q{ 91 | [ 92 | #{endpoint0_json} 93 | ] 94 | } 95 | 96 | cluster_version = "1.0" 97 | listener_version = "1.0" 98 | route_version = "1.0" 99 | endpoint_version = "1.0" 100 | 101 | def getDynamicCluster port, idx 102 | resp = RestClient.get "http://localhost:#{port}/config_dump" 103 | json = JSON.parse(resp) 104 | actual = json["configs"]["clusters"]["dynamicActiveClusters"][idx]["cluster"] 105 | actual = actual.to_snake_keys 106 | return actual 107 | end 108 | 109 | def getDynamicListener port, idx 110 | resp = RestClient.get "http://localhost:#{port}/config_dump" 111 | json = JSON.parse(resp) 112 | actual = json["configs"]["listeners"]["dynamicActiveListeners"][idx]["listener"] 113 | actual = actual.to_snake_keys 114 | return actual 115 | end 116 | 117 | def getDynamicRoute port, idx 118 | resp = RestClient.get "http://localhost:#{port}/config_dump" 119 | json = JSON.parse(resp) 120 | actual = json["configs"]["routes"]["dynamicRouteConfigs"][idx]["routeConfig"] 121 | actual = actual.to_snake_keys 122 | return actual 123 | end 124 | 125 | def getVersion port, key1, key2, idx 126 | resp = RestClient.get "http://localhost:#{port}/config_dump" 127 | json = JSON.parse(resp) 128 | actualVersion = json["configs"][key1][key2][idx]["versionInfo"] 129 | end 130 | 131 | describe "xDS" do 132 | let(:port) { 9901 } 133 | before(:all) do 134 | ['xdstest-cluster', 'adstest-cluster'].each { |type| 135 | clusterKey = "xDS/app-cluster/#{type}/CDS" 136 | listenerKey = "xDS/app-cluster/#{type}/LDS" 137 | routeKey = "xDS/app-cluster/#{type}/RDS" 138 | endpointKey = "xDS/app-cluster/#{type}/EDS" 139 | 140 | cdelete(clusterKey) 141 | cdelete(listenerKey) 142 | cdelete(routeKey) 143 | 144 | cset("#{clusterKey}/config", clusters_json) 145 | cset("#{clusterKey}/version", cluster_version) 146 | 147 | cset("#{listenerKey}/config", listeners_json) 148 | cset("#{listenerKey}/version", listener_version) 149 | 150 | cset("#{routeKey}/config", routes_json) 151 | cset("#{routeKey}/version", route_version) 152 | 153 | cset("#{endpointKey}/config", endpoints_json) 154 | cset("#{endpointKey}/version", endpoint_version) 155 | } 156 | sleep 120 unless ENV['DEVMODE'] 157 | end 158 | 159 | describe "CDS" do 160 | it "Add a cluster" do 161 | actual = getDynamicCluster(port, 0) 162 | actualVersion = getVersion(port, "clusters", "dynamicActiveClusters", 0) 163 | 164 | expected = JSON.parse(clusters_json) 165 | expected[0]["type"] = expected[0]["type"].upcase 166 | 167 | expect(actual).to eq(expected[0]) 168 | expect(actualVersion).to eq(cluster_version) 169 | end 170 | 171 | it "Add a cluster with http2 options" do 172 | actual = getDynamicCluster(port, 1) 173 | actualVersion = getVersion(port, "clusters", "dynamicActiveClusters", 1) 174 | 175 | expected = JSON.parse(clusters_json) 176 | expected[1]["type"] = expected[1]["type"].upcase 177 | alpn = expected[1]["tls_context"]["common_tls_context"]["alpn_protocols"] 178 | expected[1]["tls_context"]["common_tls_context"]["alpn_protocols"] = [alpn] 179 | 180 | expect(actual).to eq(expected[1]) 181 | expect(actualVersion).to eq(cluster_version) 182 | end 183 | 184 | it "Add a cluster without http2" do 185 | actual = getDynamicCluster(port, 2) 186 | actualVersion = getVersion(port, "clusters", "dynamicActiveClusters", 2) 187 | 188 | expected = JSON.parse(clusters_json) 189 | expected[2]["type"] = expected[2]["type"].upcase 190 | 191 | expected[2].delete("lb_policy") 192 | 193 | expect(actual).to eq(expected[2]) 194 | expect(actualVersion).to eq(cluster_version) 195 | end 196 | end 197 | 198 | describe "LDS" do 199 | it 'Add a listener without rds' do 200 | actual = getDynamicListener(port, 0) 201 | actualVersion = getVersion(port, "listeners", "dynamicActiveListeners", 0) 202 | 203 | expected = JSON.parse(listeners_json) 204 | 205 | expect(actual).to eq(expected[0]) 206 | expect(actualVersion).to eq(listener_version) 207 | end 208 | 209 | it 'Add a listener with rds' do 210 | actual = getDynamicListener(port, 1) 211 | actualVersion = getVersion(port, "listeners", "dynamicActiveListeners", 1) 212 | 213 | expected = JSON.parse(listeners_json) 214 | 215 | expect(actual).to eq(expected[1]) 216 | expect(actualVersion).to eq(listener_version) 217 | end 218 | 219 | it 'Add a listener with tls context' do 220 | actual = getDynamicListener(port, 3) 221 | actualVersion = getVersion(port, "listeners", "dynamicActiveListeners", 3) 222 | 223 | expected = JSON.parse(listeners_json)[3] 224 | alpn = expected['filter_chains'][0]['tls_context']['common_tls_context']['alpn_protocols'] 225 | expected['filter_chains'][0]['tls_context']['common_tls_context']['alpn_protocols'] = [alpn] 226 | 227 | expect(actual).to eq(expected) 228 | expect(actualVersion).to eq(listener_version) 229 | end 230 | end 231 | 232 | describe "RDS" do 233 | it 'Add a dynamic route' do 234 | actual = getDynamicRoute(port, 0) 235 | actualVersion = getVersion(port, "routes", "dynamicRouteConfigs", 0) 236 | 237 | expected = JSON.parse(routes_json) 238 | 239 | expect(actual).to eq(expected[0]) 240 | expect(actualVersion).to eq(route_version) 241 | end 242 | end 243 | 244 | describe "EDS" do 245 | it 'Add a cluster with eds' do 246 | actual = getDynamicCluster(port, 3) 247 | actualVersion = getVersion(port, "clusters", "dynamicActiveClusters", 3) 248 | 249 | expected = JSON.parse(clusters_json) 250 | expected[2]["type"] = expected[3]["type"].upcase 251 | 252 | expect(actual).to eq(expected[3]) 253 | expect(actualVersion).to eq(cluster_version) 254 | 255 | appResponse = RestClient.get "http://localhost:18123/abc" 256 | expect(appResponse.code).to eq(200) 257 | expect(appResponse.body).to eq("Responding to abc!") 258 | end 259 | end 260 | 261 | describe "Aggregated Discovery Services(ADS)" do 262 | let(:port) { 9902 } 263 | 264 | describe "CDS" do 265 | it "Add a cluster" do 266 | actual = getDynamicCluster(port, 0) 267 | actualVersion = getVersion(port, "clusters", "dynamicActiveClusters", 0) 268 | 269 | expected = JSON.parse(clusters_json) 270 | expected[0]["type"] = expected[0]["type"].upcase 271 | 272 | expect(actual).to eq(expected[0]) 273 | expect(actualVersion).to eq(cluster_version) 274 | end 275 | 276 | it "Add a cluster with http2 options" do 277 | actual = getDynamicCluster(port, 1) 278 | actualVersion = getVersion(port, "clusters", "dynamicActiveClusters", 1) 279 | 280 | expected = JSON.parse(clusters_json) 281 | expected[1]["type"] = expected[1]["type"].upcase 282 | alpn = expected[1]["tls_context"]["common_tls_context"]["alpn_protocols"] 283 | expected[1]["tls_context"]["common_tls_context"]["alpn_protocols"] = [alpn] 284 | 285 | expect(actual).to eq(expected[1]) 286 | expect(actualVersion).to eq(cluster_version) 287 | end 288 | end 289 | 290 | describe "LDS" do 291 | it 'Add a listener without rds' do 292 | actual = getDynamicListener(port, 0) 293 | actualVersion = getVersion(port, "listeners", "dynamicActiveListeners", 0) 294 | 295 | expected = JSON.parse(listeners_json) 296 | 297 | expect(actual).to eq(expected[0]) 298 | expect(actualVersion).to eq(listener_version) 299 | end 300 | 301 | it 'Add a listener with rds' do 302 | actual = getDynamicListener(port, 1) 303 | actualVersion = getVersion(port, "listeners", "dynamicActiveListeners", 1) 304 | 305 | expected = JSON.parse(listeners_json) 306 | 307 | expect(actual).to eq(expected[1]) 308 | expect(actualVersion).to eq(listener_version) 309 | end 310 | end 311 | 312 | describe "RDS" do 313 | it 'Add a dynamic route' do 314 | actual = getDynamicRoute(port, 0) 315 | 316 | expected = JSON.parse(routes_json) 317 | 318 | expect(actual).to eq(expected[0]) 319 | end 320 | end 321 | 322 | describe "EDS" do 323 | it 'Add a cluster with eds' do 324 | actual = getDynamicCluster(port, 3) 325 | actualVersion = getVersion(port, "clusters", "dynamicActiveClusters", 3) 326 | 327 | expected = JSON.parse(clusters_json) 328 | expected[2]["type"] = expected[3]["type"].upcase 329 | 330 | expect(actual).to eq(expected[3]) 331 | expect(actualVersion).to eq(cluster_version) 332 | 333 | appResponse = RestClient.get "http://localhost:28123/abc" 334 | expect(appResponse.code).to eq(200) 335 | expect(appResponse.body).to eq("Responding to abc!") 336 | end 337 | end 338 | end 339 | end 340 | 341 | def cset key, val 342 | Diplomat::Kv.put(key, val) 343 | end 344 | 345 | def cdelete key 346 | Diplomat::Kv.delete(key) 347 | end -------------------------------------------------------------------------------- /test/rspec/json/cluster_0.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app1", 3 | "connect_timeout": "0.250s", 4 | "type": "strict_dns", 5 | "lb_policy": "RANDOM", 6 | "http2_protocol_options": {}, 7 | "hosts": [{ 8 | "socket_address": { 9 | "address": "127.0.0.2", 10 | "port_value": 1234 11 | } 12 | }] 13 | } -------------------------------------------------------------------------------- /test/rspec/json/cluster_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app1-grpc", 3 | "connect_timeout": "0.250s", 4 | "type": "strict_dns", 5 | "lb_policy": "RANDOM", 6 | "http2_protocol_options": { 7 | "hpack_table_size": 12, 8 | "max_concurrent_streams": 14, 9 | "initial_stream_window_size": 268435456, 10 | "initial_connection_window_size": 268435456 11 | }, 12 | "tls_context": { 13 | "common_tls_context": { 14 | "tls_certificates": [{ 15 | "certificate_chain": { 16 | "filename": "/etc/cert/cert.pem" 17 | }, 18 | "private_key": { 19 | "filename": "/etc/pkey/pkey.pem" 20 | } 21 | }], 22 | "alpn_protocols": "h2" 23 | }, 24 | "sni": "www.examplehost.com" 25 | }, 26 | "hosts": [{ 27 | "socket_address": { 28 | "address": "127.0.0.2", 29 | "port_value": 1234 30 | } 31 | }] 32 | } -------------------------------------------------------------------------------- /test/rspec/json/cluster_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app3", 3 | "connect_timeout": "0.250s", 4 | "lb_policy": "ROUND_ROBIN", 5 | "type": "strict_dns", 6 | "hosts": [{ 7 | "socket_address": { 8 | "address": "app-server", 9 | "port_value": 8123 10 | } 11 | }], 12 | "circuit_breakers": { 13 | "thresholds": [ 14 | { 15 | "priority": "HIGH", 16 | "max_connections": 2045, 17 | "max_pending_requests": 2046, 18 | "max_requests": 2047, 19 | "max_retries": 2048 20 | } 21 | ] 22 | } 23 | } -------------------------------------------------------------------------------- /test/rspec/json/cluster_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app4", 3 | "connect_timeout": "0.250s", 4 | "lb_policy": "RANDOM", 5 | "type": "EDS", 6 | "eds_cluster_config": { 7 | "eds_config": { 8 | "api_config_source": { 9 | "api_type": "GRPC", 10 | "grpc_services": [{ 11 | "envoy_grpc": { 12 | "cluster_name": "xds_cluster" 13 | } 14 | }] 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /test/rspec/json/listener_0.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "listener_0", 3 | "address": { 4 | "socket_address": { 5 | "address": "0.0.0.0", 6 | "port_value": 80 7 | } 8 | }, 9 | "filter_chains": [ 10 | { 11 | "filters": [ 12 | { 13 | "name": "envoy.http_connection_manager", 14 | "config": { 15 | "stat_prefix": "ingress_http", 16 | "tracing": { 17 | "operation_name": "EGRESS" 18 | }, 19 | "access_log": [ 20 | { 21 | "name": "envoy.file_access_log", 22 | "config": { 23 | "path": "/dev/stdout", 24 | "format": "[ACCESS_LOG][%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\" \"%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%\"\n" 25 | } 26 | } 27 | ], 28 | "codec_type": "HTTP2", 29 | "route_config": { 30 | "name": "local_http_router", 31 | "virtual_hosts": [ 32 | { 33 | "name": "local_service", 34 | "domains": [ 35 | "*" 36 | ], 37 | "routes": [ 38 | { 39 | "match": { 40 | "prefix": "/" 41 | }, 42 | "route": { 43 | "cluster": "app1" 44 | } 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | "http_filters": [ 51 | { 52 | "name": "envoy.health_check", 53 | "config": { 54 | "pass_through_mode": false, 55 | "endpoint": "/healthz" 56 | } 57 | }, 58 | { 59 | "name": "envoy.router" 60 | } 61 | ] 62 | } 63 | } 64 | ] 65 | } 66 | ] 67 | } -------------------------------------------------------------------------------- /test/rspec/json/listener_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "listener_1", 3 | "address": { 4 | "socket_address": { 5 | "address": "127.0.0.1", 6 | "port_value": 10001 7 | } 8 | }, 9 | "filter_chains": [ 10 | { 11 | "filters": [ 12 | { 13 | "name": "envoy.http_connection_manager", 14 | "config": { 15 | "stat_prefix": "ingress_http", 16 | "access_log": [ 17 | { 18 | "name": "envoy.file_access_log", 19 | "config": { 20 | "path": "/dev/stdout", 21 | "format": "some-format" 22 | } 23 | } 24 | ], 25 | "codec_type": "HTTP2", 26 | "rds": { 27 | "route_config_name": "listener_1_route", 28 | "config_source": { 29 | "api_config_source": { 30 | "api_type": "GRPC", 31 | "grpc_services": [{ 32 | "envoy_grpc": { 33 | "cluster_name": "xds_cluster" 34 | } 35 | }] 36 | } 37 | } 38 | }, 39 | "http_filters": [ 40 | { 41 | "name": "envoy.router" 42 | } 43 | ] 44 | } 45 | } 46 | ] 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /test/rspec/json/listener_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "listener_2", 3 | "address": { 4 | "socket_address": { 5 | "address": "0.0.0.0", 6 | "port_value": 18123 7 | } 8 | }, 9 | "filter_chains": [ 10 | { 11 | "filters": [ 12 | { 13 | "name": "envoy.http_connection_manager", 14 | "config": { 15 | "stat_prefix": "ingress_http", 16 | "codec_type": "auto", 17 | "generate_request_id": true, 18 | "route_config": { 19 | "name": "local_http_router", 20 | "virtual_hosts": [ 21 | { 22 | "name": "local_service", 23 | "domains": [ 24 | "*" 25 | ], 26 | "routes": [ 27 | { 28 | "match": { 29 | "prefix": "/" 30 | }, 31 | "route": { 32 | "cluster": "app3" 33 | } 34 | } 35 | ] 36 | } 37 | ] 38 | }, 39 | "http_filters": [ 40 | { 41 | "name": "envoy.router" 42 | } 43 | ] 44 | } 45 | } 46 | ] 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /test/rspec/json/listener_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "listener_3", 3 | "address": { 4 | "socket_address": { 5 | "address": "0.0.0.0", 6 | "port_value": 443 7 | } 8 | }, 9 | "filter_chains": [ 10 | { 11 | "tls_context": { 12 | "common_tls_context": { 13 | "tls_certificates": [{ 14 | "certificate_chain": { 15 | "filename": "/etc/cert/cert.pem" 16 | }, 17 | "private_key": { 18 | "filename": "/etc/pkey/pkey.pem" 19 | } 20 | }], 21 | "alpn_protocols": "h2" 22 | } 23 | }, 24 | "filters": [ 25 | { 26 | "name": "envoy.http_connection_manager", 27 | "config": { 28 | "stat_prefix": "ingress_http", 29 | "tracing": { 30 | "operation_name": "EGRESS" 31 | }, 32 | "access_log": [ 33 | { 34 | "name": "envoy.file_access_log", 35 | "config": { 36 | "path": "/dev/stdout", 37 | "format": "[ACCESS_LOG][%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\" \"%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%\"\n" 38 | } 39 | } 40 | ], 41 | "codec_type": "HTTP2", 42 | "route_config": { 43 | "name": "local_http_router", 44 | "virtual_hosts": [ 45 | { 46 | "name": "local_service", 47 | "domains": [ 48 | "*" 49 | ], 50 | "routes": [ 51 | { 52 | "match": { 53 | "prefix": "/" 54 | }, 55 | "route": { 56 | "weighted_clusters": { 57 | "runtime_key_prefix": "routing.traffic_split.app1", 58 | "total_weight": 100, 59 | "clusters": [ 60 | { 61 | "name": "app1", 62 | "weight": 50 63 | }, 64 | { 65 | "name": "app3", 66 | "weight": 50 67 | } 68 | ] 69 | }, 70 | "timeout": "30s" 71 | } 72 | } 73 | ] 74 | } 75 | ] 76 | }, 77 | "http_filters": [ 78 | { 79 | "name": "envoy.health_check", 80 | "config": { 81 | "pass_through_mode": false, 82 | "endpoint": "/healthz" 83 | } 84 | }, 85 | { 86 | "name": "envoy.router" 87 | } 88 | ] 89 | } 90 | } 91 | ] 92 | } 93 | ] 94 | } --------------------------------------------------------------------------------