├── .gitignore ├── .travis.yml ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── Vagrantfile ├── ansible ├── README.md ├── ansible.cfg ├── group_vars │ ├── all.yml │ └── vagrant.yml ├── inventories │ └── vagrant ├── requirements.txt ├── roles │ ├── common │ │ ├── defaults │ │ │ └── main.yml │ │ └── tasks │ │ │ ├── CentOS.yml │ │ │ └── main.yml │ ├── consul │ │ ├── defaults │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ ├── client.json.j2 │ │ │ ├── consul.service.j2 │ │ │ └── server.json.j2 │ ├── docker │ │ ├── defaults │ │ │ └── main.yml │ │ └── tasks │ │ │ ├── CentOS-7.yml │ │ │ ├── compose.yml │ │ │ └── main.yml │ ├── grafana │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ ├── envoy-global.json │ │ │ └── envoy-service-to-service.json │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ ├── configure.yml │ │ │ └── main.yml │ │ ├── templates │ │ │ └── grafana.ini.j2 │ │ └── vars │ │ │ └── main.yml │ ├── meshem │ │ ├── defaults │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ ├── meshem.service.j2 │ │ │ └── meshem.yaml.j2 │ ├── prometheus │ │ ├── defaults │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ ├── prometheus.service.j2 │ │ │ └── prometheus.yml.j2 │ └── zipkin │ │ ├── defaults │ │ └── main.yml │ │ ├── tasks │ │ └── main.yml │ │ └── templates │ │ └── docker-compose.yml.j2 └── site.yml ├── examples ├── ansible │ ├── ansible.cfg │ ├── data-plane.yml │ ├── group_vars │ │ ├── app.yml │ │ ├── front.yml │ │ └── vagrant.yml │ ├── meshem-conf │ │ ├── app.yaml │ │ └── front.yaml │ ├── requirements.txt │ ├── roles │ │ ├── app │ │ │ ├── handlers │ │ │ │ └── main.yml │ │ │ ├── meta │ │ │ │ └── main.yml │ │ │ ├── tasks │ │ │ │ └── main.yml │ │ │ └── templates │ │ │ │ ├── app.go.j2 │ │ │ │ └── app.service.j2 │ │ ├── common │ │ ├── docker │ │ ├── envoy │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ ├── handlers │ │ │ │ └── main.yml │ │ │ ├── tasks │ │ │ │ └── main.yml │ │ │ └── templates │ │ │ │ ├── docker-compose.yml.j2 │ │ │ │ └── envoy-conf.yaml.j2 │ │ ├── front │ │ │ ├── handlers │ │ │ │ └── main.yml │ │ │ ├── meta │ │ │ │ └── main.yml │ │ │ ├── tasks │ │ │ │ └── main.yml │ │ │ └── templates │ │ │ │ ├── front.go.j2 │ │ │ │ └── front.service.j2 │ │ └── go │ │ │ ├── defaults │ │ │ └── main.yml │ │ │ ├── tasks │ │ │ └── main.yml │ │ │ └── templates │ │ │ └── go.sh.j2 │ ├── site.yml │ └── vagrant └── docker │ ├── Dockerfile-client │ ├── Dockerfile-envoy │ ├── Dockerfile-meshem │ ├── app1 │ ├── Dockerfile │ ├── app1-svc.yml │ └── main.go │ ├── docker-compose.yml │ ├── envoy.yaml │ ├── front │ ├── Dockerfile │ ├── front-svc.yml │ └── main.go │ ├── meshem.yaml │ └── run.sh ├── images ├── control-plane.png ├── data-plane1.png └── data-plane2.png ├── release.sh ├── run.sh └── src ├── core ├── ctlapi │ ├── client.go │ ├── rest_services.go │ ├── rest_services_test.go │ └── server.go ├── inventory.go ├── inventory_test.go ├── version_gen.go └── xds │ ├── hasher.go │ ├── healthcheck.go │ ├── server.go │ ├── snapshot_gen.go │ └── snapshot_gen_test.go ├── meshem └── main.go ├── meshemctl ├── command │ ├── apiclient.go │ ├── error.go │ ├── svc_cmd.go │ └── version_cmd.go └── main.go ├── model ├── address.go ├── address_test.go ├── config.go ├── host.go ├── host_test.go ├── service.go ├── service_test.go └── version.go ├── repository ├── discovery_consul.go ├── discovery_consul_test.go ├── inventory_consul.go ├── inventory_heap.go ├── inventory_test.go └── repository.go ├── start-mock-consul.sh ├── stop-mock-consul.sh ├── utils ├── consul.go ├── consul_test.go ├── slice.go └── slice_test.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | .python-version 3 | vendor/ 4 | meshem*_darwin_* 5 | meshem*_windows_* 6 | meshem*_linux_* 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: required 3 | go: 4 | - 1.9 5 | services: 6 | - docker 7 | before_install: 8 | - go get github.com/golang/dep/... 9 | - go get github.com/mitchellh/gox 10 | install: 11 | - $GOPATH/bin/dep ensure 12 | script: 13 | - sh ./src/start-mock-consul.sh 14 | - go test -v -short ./... 15 | - sh ./src/stop-mock-consul.sh 16 | deploy: 17 | - provider: script 18 | script: bash release.sh "rerorero/meshem" "$TRAVIS_TAG" 19 | skip_cleanup: true 20 | on: 21 | tags: true 22 | repo: rerorero/meshem 23 | - provider: releases 24 | api_key: 25 | secure: goWQAXS+K/pCc1gUPTyUJVAOO4eqgkGBSKbnqbKaaWCUjcczCY353trdLmxNqG0IVQOC0g+DaQ3mC2DkM+I+4qsnf/wY81FqdwJJy6Ol2ffiviM4ecvztDDI102VT1k6M8sMk5+25b1tUPy5kpbxhb2OxeE+Wv7lu4hTYBA0EA18+JHnE8GTbJHCOG5m44GW05o1bknC1brCYOUUyu6v1np/JaieGf/mxatPWn2LVl7l8AaYY4dRpA08lx18R8oC6DWYP2c2DoAU3M+mRKOv0W/RIALSWufPCC7Uhk0/fy1tTyHYf0fv/6NZ50f5O2qK/JXvxhZIdT+hH88lJa95kRaK8FieAPOaDX2GKC1wyZBwcewTEAyOlsFO6WtGK1E8SLunZ7B+CIMafx60gLMwrnTcMkmWgvh/qALXM1aXmlDlpdxCoe5w6ZyVUEIFGiDCzTZR2pbZcQZgXeaXwaFuo91b7CA6FKeetinmLwBYHtW+rcqi1NGehdabAlhIE1aWmSfhCSN7zyu6s64s5m1d1tzBByoEsDctS6hFGz7R9PL++xEw2m1EjNtKkJ93YLJQSNKy9/1Y5oYL+yDIGZhNg0wa+DbveKYv7PdoSY4ZZGQ6SjEbc95duPSakWkGAXlNF6wkl1XoJ+e1E/mwb5wNxAx8slHgpsN3UeGoSbDboRE= 26 | file: 27 | - meshem_darwin_amd64 28 | - meshem_linux_amd64 29 | - meshem_windows_amd64.exe 30 | - meshemctl_darwin_amd64 31 | - meshemctl_linux_amd64 32 | - meshemctl_windows_amd64.exe 33 | skip_cleanup: true 34 | on: 35 | tags: true 36 | repo: rerorero/meshem 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.9 as builder 2 | 3 | # build directories 4 | RUN mkdir -p /go/src/github.com/rerorero/meshem 5 | WORKDIR /go/src/github.com/rerorero/meshem 6 | ADD . . 7 | 8 | # Build meshem 9 | RUN go get github.com/golang/dep/... 10 | RUN dep ensure 11 | RUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -installsuffix netgo --ldflags '-extldflags "-static"' -o /meshem ./src/meshem 12 | RUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -installsuffix netgo --ldflags '-extldflags "-static"' -o /meshemctl ./src/meshemctl 13 | 14 | # runner container 15 | FROM alpine:latest 16 | COPY --from=builder /meshem /bin/meshem 17 | COPY --from=builder /meshemctl /bin/meshemctl 18 | -------------------------------------------------------------------------------- /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/davecgh/go-spew" 6 | packages = ["spew"] 7 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 8 | version = "v1.1.0" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/envoyproxy/go-control-plane" 13 | packages = ["envoy/api/v2","envoy/api/v2/auth","envoy/api/v2/cluster","envoy/api/v2/core","envoy/api/v2/endpoint","envoy/api/v2/listener","envoy/api/v2/route","envoy/config/filter/accesslog/v2","envoy/config/filter/http/health_check/v2","envoy/config/filter/network/http_connection_manager/v2","envoy/config/filter/network/tcp_proxy/v2","envoy/service/discovery/v2","envoy/type","pkg/cache","pkg/log","pkg/server","pkg/util"] 14 | revision = "999f0991b6aea8c5485df31682b8adbdba1ecd07" 15 | 16 | [[projects]] 17 | branch = "master" 18 | name = "github.com/gogo/googleapis" 19 | packages = ["google/api","google/rpc"] 20 | revision = "0cd9801be74a10d5ac39d69626eac8255ffcd502" 21 | 22 | [[projects]] 23 | name = "github.com/gogo/protobuf" 24 | packages = ["gogoproto","jsonpb","proto","protoc-gen-gogo/descriptor","sortkeys","types"] 25 | revision = "1adfc126b41513cc696b209667c8656ea7aac67c" 26 | version = "v1.0.0" 27 | 28 | [[projects]] 29 | name = "github.com/golang/protobuf" 30 | packages = ["proto","protoc-gen-go/descriptor","ptypes","ptypes/any","ptypes/duration","ptypes/timestamp"] 31 | revision = "925541529c1fa6821df4e44ce2723319eb2be768" 32 | version = "v1.0.0" 33 | 34 | [[projects]] 35 | name = "github.com/hashicorp/consul" 36 | packages = ["api"] 37 | revision = "9a494b5fb9c86180a5702e29c485df1507a47198" 38 | version = "v1.0.6" 39 | 40 | [[projects]] 41 | branch = "master" 42 | name = "github.com/hashicorp/go-cleanhttp" 43 | packages = ["."] 44 | revision = "d5fe4b57a186c716b0e00b8c301cbd9b4182694d" 45 | 46 | [[projects]] 47 | branch = "master" 48 | name = "github.com/hashicorp/go-rootcerts" 49 | packages = ["."] 50 | revision = "6bb64b370b90e7ef1fa532be9e591a81c3493e00" 51 | 52 | [[projects]] 53 | name = "github.com/hashicorp/serf" 54 | packages = ["coordinate"] 55 | revision = "d6574a5bb1226678d7010325fb6c985db20ee458" 56 | version = "v0.8.1" 57 | 58 | [[projects]] 59 | name = "github.com/inconshreveable/mousetrap" 60 | packages = ["."] 61 | revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" 62 | version = "v1.0" 63 | 64 | [[projects]] 65 | name = "github.com/julienschmidt/httprouter" 66 | packages = ["."] 67 | revision = "8c199fb6259ffc1af525cc3ad52ee60ba8359669" 68 | version = "v1.1" 69 | 70 | [[projects]] 71 | name = "github.com/lyft/protoc-gen-validate" 72 | packages = ["validate"] 73 | revision = "930a67cf7ba41b9d9436ad7a1be70a5d5ff6e1fc" 74 | version = "v0.0.6" 75 | 76 | [[projects]] 77 | branch = "master" 78 | name = "github.com/mitchellh/go-homedir" 79 | packages = ["."] 80 | revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" 81 | 82 | [[projects]] 83 | name = "github.com/pkg/errors" 84 | packages = ["."] 85 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d" 86 | version = "v0.8.0" 87 | 88 | [[projects]] 89 | name = "github.com/pmezard/go-difflib" 90 | packages = ["difflib"] 91 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 92 | version = "v1.0.0" 93 | 94 | [[projects]] 95 | name = "github.com/sirupsen/logrus" 96 | packages = ["."] 97 | revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" 98 | version = "v1.0.5" 99 | 100 | [[projects]] 101 | name = "github.com/spf13/cobra" 102 | packages = ["."] 103 | revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" 104 | version = "v0.0.1" 105 | 106 | [[projects]] 107 | name = "github.com/spf13/pflag" 108 | packages = ["."] 109 | revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" 110 | version = "v1.0.0" 111 | 112 | [[projects]] 113 | name = "github.com/stretchr/objx" 114 | packages = ["."] 115 | revision = "facf9a85c22f48d2f52f2380e4efce1768749a89" 116 | version = "v0.1" 117 | 118 | [[projects]] 119 | name = "github.com/stretchr/testify" 120 | packages = ["assert","mock"] 121 | revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" 122 | version = "v1.2.1" 123 | 124 | [[projects]] 125 | branch = "master" 126 | name = "golang.org/x/crypto" 127 | packages = ["ssh/terminal"] 128 | revision = "182114d582623c1caa54f73de9c7224e23a48487" 129 | 130 | [[projects]] 131 | branch = "master" 132 | name = "golang.org/x/net" 133 | packages = ["context","http2","http2/hpack","idna","internal/timeseries","lex/httplex","trace"] 134 | revision = "803fdb99c0f72e493c28ef2099d250a9c989d8ff" 135 | 136 | [[projects]] 137 | branch = "master" 138 | name = "golang.org/x/sys" 139 | packages = ["unix","windows"] 140 | revision = "8c0ece68c28377f4c326d85b94f8df0dace46f80" 141 | 142 | [[projects]] 143 | name = "golang.org/x/text" 144 | packages = ["collate","collate/build","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","language","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"] 145 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 146 | version = "v0.3.0" 147 | 148 | [[projects]] 149 | branch = "master" 150 | name = "google.golang.org/genproto" 151 | packages = ["googleapis/rpc/status"] 152 | revision = "df60624c1e9b9d2973e889c7a1cff73155da81c4" 153 | 154 | [[projects]] 155 | name = "google.golang.org/grpc" 156 | packages = [".","balancer","balancer/base","balancer/roundrobin","codes","connectivity","credentials","encoding","encoding/proto","grpclb/grpc_lb_v1/messages","grpclog","internal","keepalive","metadata","naming","peer","resolver","resolver/dns","resolver/passthrough","stats","status","tap","transport"] 157 | revision = "8e4536a86ab602859c20df5ebfd0bd4228d08655" 158 | version = "v1.10.0" 159 | 160 | [[projects]] 161 | name = "gopkg.in/yaml.v2" 162 | packages = ["."] 163 | revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5" 164 | version = "v2.1.1" 165 | 166 | [solve-meta] 167 | analyzer-name = "dep" 168 | analyzer-version = 1 169 | inputs-digest = "7e4d0ed369bd0393e5de38bb2cf74d155041b017bb486fa6d9f9564c4a178862" 170 | solver-name = "gps-cdcl" 171 | solver-version = 1 172 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | branch = "master" 26 | name = "github.com/envoyproxy/go-control-plane" 27 | 28 | [[constraint]] 29 | name = "github.com/hashicorp/consul" 30 | version = "1.0.6" 31 | 32 | [[constraint]] 33 | name = "github.com/pkg/errors" 34 | version = "0.8.0" 35 | 36 | [[constraint]] 37 | name = "github.com/sirupsen/logrus" 38 | version = "1.0.4" 39 | 40 | [[constraint]] 41 | name = "github.com/spf13/cobra" 42 | version = "0.0.1" 43 | 44 | [[constraint]] 45 | name = "github.com/stretchr/testify" 46 | version = "1.2.1" 47 | 48 | [[constraint]] 49 | name = "google.golang.org/grpc" 50 | version = "1.10.0" 51 | 52 | [[constraint]] 53 | name = "gopkg.in/yaml.v2" 54 | version = "2.1.1" 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | meshem 2 | ======= 3 | [![Build Status](https://travis-ci.org/rerorero/meshem.svg?branch=master)](https://travis-ci.org/rerorero/meshem) 4 | 5 | meshem is a simple service mesh control plane application which depends on [Envoy](https://www.envoyproxy.io/). This project consists of the followings. 6 | - [meshem server (xds and API)](https://github.com/rerorero/meshem/releases) 7 | - [meshem CLI (meshemctl)](https://github.com/rerorero/meshem/releases) 8 | - [Ansible playbooks that deploys control plane components.](/ansible) 9 | - [Example ansible playbooks of data planes.](/exampls/ansible) 10 | - [Docker Example.](/exampls/docker) 11 | 12 | You can get meshem server and CLI binary from [Github release page](https://github.com/rerorero/meshem/releases). There is also a docker image that contains both of them. Try `docker pull rerorero/meshem`. 13 | 14 | This implementation is not production ready as the purpose of meshem is mainly to learn envoy and service mesh. 15 | 16 | Vagrant + Ansible example: Oneshot 17 | ======= 18 | ``` 19 | go get github.com/rerorero/meshem 20 | cd $GOPATH/src/github.com/rerorero/meshem 21 | ./run.sh 22 | ``` 23 | 24 | Vagrant + Ansible example: Step by step 25 | ======= 26 | 27 | ### Requirments 28 | - Vagrant + Virtual Box 29 | - python 30 | 31 | Create all VMs. 32 | ``` 33 | vagrant up 34 | ``` 35 | 36 | ### Launch example services without proxy 37 | Provision example services without envoy proxy. The example consists of `app` service and `front` service, both of them provide HTTP API. 38 | ``` 39 | cd ./examples/ansible 40 | pip install -r requirements.txt 41 | ansible-playbook -i vagrant site.yml 42 | # default timezone is JST. You can change this by passing 'common_timezone' argument. 43 | # ansible-playbook -i vagrant site.yml -e "common_timezone=UTC" 44 | ``` 45 | Check the response from front application. 46 | ``` 47 | curl 192.168.34.70:8080 48 | ``` 49 | At this point the service dependencies are as follows (`myapp2` is not used yet). 50 | 51 | 52 | ### Let's mesh'em 53 | Provision the meshem control plane. 54 | ``` 55 | cd ./ansible 56 | ansible-playbook -i inventories/vagrant site.yml 57 | ``` 58 | Launch envoy proxies for each services. 59 | ``` 60 | cd ./examples/ansible 61 | ansible-playbook -i vagrant data-plane.yml 62 | ``` 63 | 64 | #### Manage meshem 65 | Download meshem CLI binary(meshemctl) from [Github release page](https://github.com/rerorero/meshem/releases) and put it somewhere in your `$PATH`. meshemctl also can be built from source by running `go install github.com/rerorero/meshem/src/meshemctl` command. 66 | 67 | Then let us regsiter the app service and front service. 68 | ``` 69 | cd ./examples/ansible 70 | export MESHEM_CTLAPI_ENDPOINT="http://192.168.34.61:8091" 71 | meshemctl svc apply app -f ./meshem-conf/app.yaml 72 | meshemctl svc apply front -f ./meshem-conf/front.yaml 73 | ``` 74 | 75 | #### Deploy services as a service mesh 76 | Deploy the front application sot that it uses envoy as egress proxy. 77 | ``` 78 | ansible-playbook -i vagrant site.yml -e "front_app_endpoint=http://127.0.0.1:9001/" 79 | ``` 80 | Currently the service dependencies are as follows. 81 | 82 | 83 | 84 | The following figure shows the relationship between the control plane and the data plane. 85 | 86 | 87 | 88 | #### Send a request 89 | Try to send HTTP requests to front proxy several times. From the response you can confirm that the requests are round robin balanced. 90 | ``` 91 | curl 192.168.34.70:80 92 | ``` 93 | Envoy metrics and trace results can be displayed. 94 | - Zipkin is running on http://192.168.34.62:9411/ 95 | - Grafana is running on http://192.168.34.62:3000/ 96 | - Prometheus is running on http://192.168.34.62:9090/ 97 | - Dashoboard uses [transferwise/prometheus-envoy-dashboards](https://github.com/transferwise/prometheus-envoy-dashboards). Thanks! 98 | 99 | Docker example 100 | ======= 101 | Docker exapmle starts containers and build meshem binary from local source code. 102 | ``` 103 | go get github.com/rerorero/meshem 104 | go get github.com/golang/dep/cmd/dep 105 | cd $GOPATH/src/github.com/rerorero/meshem 106 | dep ensure 107 | cd ./examples/docker 108 | # run.sh starts all of the relevant containers and register example services. 109 | ./run.sh 110 | ``` 111 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | # 4 | Vagrant::DEFAULT_SERVER_URL.replace('https://vagrantcloud.com') 5 | 6 | Vagrant.configure("2") do |config| 7 | 8 | hosts = [ 9 | {name: "meshem-meshem", ip: "192.168.34.61"}, 10 | {name: "meshem-monitor", ip: "192.168.34.62"}, 11 | {name: "myfront", ip: "192.168.34.70"}, 12 | {name: "myapp1", ip: "192.168.34.71"}, 13 | {name: "myapp2", ip: "192.168.34.72"}, 14 | ] 15 | 16 | 17 | def configure(c, hostname, addr) 18 | c.vm.box = "centos/7" 19 | c.vm.hostname = hostname 20 | c.vm.network :private_network, ip: addr 21 | c.vm.provision "shell", inline: "sudo systemctl restart network" 22 | c.vm.box_check_update = false 23 | c.vm.provider :virtualbox do |vb| 24 | vb.name = hostname 25 | vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 26 | end 27 | provision_script=< {{ prometheus_log_dir }}/prometheus.log 2>&1" 17 | 18 | ExecReload=/bin/kill -HUP $MAINPID 19 | 20 | [Install] 21 | WantedBy=multi-user.target 22 | -------------------------------------------------------------------------------- /ansible/roles/prometheus/templates/prometheus.yml.j2: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | scrape_timeout: 10s 4 | evaluation_interval: 1m 5 | 6 | scrape_configs: 7 | - job_name: dataplane 8 | consul_sd_configs: 9 | - server: 'localhost:8500' 10 | token: '{{ consul_master_token }}' 11 | services: ['meshem_envoy'] 12 | metrics_path: '/stats' 13 | params: 14 | format: ['prometheus'] 15 | relabel_configs: 16 | - source_labels: [__meta_consul_address, __meta_consul_service_port] 17 | separator: ':' 18 | target_label: __address__ 19 | - source_labels: ['__meta_consul_node'] 20 | target_label: 'instance' 21 | - source_labels: ['__meta_consul_metadata_meshem_service'] 22 | target_label: 'local_cluster' 23 | metric_relabel_configs: 24 | - action: 'labelmap' 25 | regex: 'envoy_(.*)' 26 | replacement: '$1' 27 | - action: 'labeldrop' 28 | regex: 'envoy_(.*)' 29 | -------------------------------------------------------------------------------- /ansible/roles/zipkin/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | zipkin_port: 9411 3 | zipkin_data_dir: "/var/zipkin" 4 | zipkin_docker_runner: docker 5 | -------------------------------------------------------------------------------- /ansible/roles/zipkin/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: mkdir zipkin directory 3 | file: 4 | path: "{{ item }}" 5 | state: directory 6 | mode: 0777 7 | recurse: yes 8 | with_items: 9 | - "{{ zipkin_data_dir }}" 10 | become: yes 11 | 12 | # TODO: deploy decently 13 | - name: Put zipkin compose file 14 | template: 15 | src: docker-compose.yml.j2 16 | dest: "{{ zipkin_data_dir }}/docker-compose.yml" 17 | 18 | - name: Start zipkin server 19 | command: docker-compose up -d 20 | args: 21 | chdir: "{{ zipkin_data_dir }}" 22 | become: yes 23 | become_user: "{{ zipkin_docker_runner }}" 24 | -------------------------------------------------------------------------------- /ansible/roles/zipkin/templates/docker-compose.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2' 3 | services: 4 | zipkin: 5 | image: openzipkin/zipkin 6 | ports: 7 | - "{{ zipkin_port }}:9411" 8 | restart: always 9 | -------------------------------------------------------------------------------- /ansible/site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: control-plane 3 | roles: 4 | - common 5 | - docker 6 | 7 | - hosts: consul 8 | roles: 9 | - consul 10 | 11 | - hosts: prometheus 12 | roles: 13 | - role: consul 14 | consul_role: client 15 | - prometheus 16 | - grafana 17 | 18 | - hosts: zipkin 19 | roles: 20 | - role: zipkin 21 | 22 | - hosts: meshem 23 | roles: 24 | - meshem 25 | -------------------------------------------------------------------------------- /examples/ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | transport = paramiko 3 | host_key_checking = False 4 | retry_files_enabled = False 5 | -------------------------------------------------------------------------------- /examples/ansible/data-plane.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: data-plane 3 | roles: 4 | - common 5 | - docker 6 | - envoy 7 | -------------------------------------------------------------------------------- /examples/ansible/group_vars/app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | envoy_service: app 3 | -------------------------------------------------------------------------------- /examples/ansible/group_vars/front.yml: -------------------------------------------------------------------------------- 1 | --- 2 | envoy_service: front 3 | -------------------------------------------------------------------------------- /examples/ansible/group_vars/vagrant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ansible_user: vagrant 3 | ansible_ssh_pass: vagrant 4 | 5 | # app service 6 | # app_port: 'app' application process listens on this port 7 | app_port: 9000 8 | 9 | # front service 10 | # front_port: 'front' application process listens on this port 11 | front_port: 8080 12 | # front_app_endpoint: 'front' application call HTTP API 13 | front_app_endpoint: "http://{{ hostvars[groups.app[0]].ansible_host }}:{{ app_port }}/" 14 | 15 | meshem_xds_endpoint: 16 | type: static 17 | endpoints: 18 | - host: 192.168.34.61 19 | port: 8090 20 | 21 | # DNS can also be used. 22 | # 23 | # meshem_xds_endpoint: 24 | # type: strict_dns 25 | # endpoints: 26 | # - host: my.xds.domain 27 | # port: 8000 28 | 29 | zipkin_endpoint: 30 | type: static 31 | endpoints: 32 | - host: 192.168.34.62 33 | port: 9411 34 | -------------------------------------------------------------------------------- /examples/ansible/meshem-conf/app.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | protocol: HTTP 3 | hosts: 4 | - name: myapp1 5 | ingressAddr: 6 | host: 192.168.34.71 7 | port: 80 8 | substanceAddr: 9 | host: 127.0.0.1 10 | port: 9000 11 | egressHost: 127.0.0.1 12 | - name: myapp2 13 | ingressAddr: 14 | host: 192.168.34.72 15 | port: 80 16 | substanceAddr: 17 | host: 127.0.0.1 18 | port: 9000 19 | egressHost: 127.0.0.1 20 | -------------------------------------------------------------------------------- /examples/ansible/meshem-conf/front.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | protocol: HTTP 3 | hosts: 4 | - name: myfront 5 | ingressAddr: 6 | host: 192.168.34.70 7 | port: 80 8 | substanceAddr: 9 | host: 127.0.0.1 10 | port: 8080 11 | egressHost: 0.0.0.0 12 | dependentServices: 13 | - name: app 14 | egressPort: 9001 15 | 16 | -------------------------------------------------------------------------------- /examples/ansible/requirements.txt: -------------------------------------------------------------------------------- 1 | Ansible==2.3 2 | -------------------------------------------------------------------------------- /examples/ansible/roles/app/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart app 3 | systemd: 4 | name: app.service 5 | state: restarted 6 | daemon_reload: yes 7 | become: yes 8 | -------------------------------------------------------------------------------- /examples/ansible/roles/app/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: go 4 | -------------------------------------------------------------------------------- /examples/ansible/roles/app/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Put Go source 3 | template: 4 | src: app.go.j2 5 | dest: /var/app.go 6 | become: yes 7 | notify: restart app 8 | 9 | - name: setup systemd 10 | template: 11 | src: app.service.j2 12 | dest: /etc/systemd/system/app.service 13 | owner: root 14 | group: root 15 | mode: 0644 16 | become: yes 17 | notify: restart app 18 | 19 | - name: starts app 20 | systemd: 21 | name: app.service 22 | state: started 23 | enabled: yes 24 | daemon_reload: yes 25 | become: yes 26 | -------------------------------------------------------------------------------- /examples/ansible/roles/app/templates/app.go.j2: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | func handler(w http.ResponseWriter, r *http.Request) { 12 | json := fmt.Sprintf(`{"msg": "app info", "from" : "%s"}`, os.Getenv("HOSTNAME")) 13 | fmt.Fprintf(w, json) 14 | } 15 | 16 | func newLogHandler(handler http.Handler) http.Handler { 17 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | log.Printf("%s %s %s header=%+v", r.RemoteAddr, r.Method, r.URL, r.Header) 19 | handler.ServeHTTP(w, r) 20 | }) 21 | } 22 | 23 | var port = flag.Int("port", 9000, "listen port") 24 | 25 | func main() { 26 | flag.Parse() 27 | http.HandleFunc("/", handler) 28 | err := http.ListenAndServe(fmt.Sprintf(":%d",*port), newLogHandler(http.DefaultServeMux)) 29 | if err != nil { 30 | println(err.Error) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/ansible/roles/app/templates/app.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=products 3 | Requires=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | Environment=HOSTNAME={{ inventory_hostname }} 8 | Restart=on-failure 9 | ExecStart={{ go_root }}/bin/go run /var/app.go --port={{ app_port }} 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | 14 | -------------------------------------------------------------------------------- /examples/ansible/roles/common: -------------------------------------------------------------------------------- 1 | ../../../ansible/roles/common -------------------------------------------------------------------------------- /examples/ansible/roles/docker: -------------------------------------------------------------------------------- 1 | ../../../ansible/roles/docker -------------------------------------------------------------------------------- /examples/ansible/roles/envoy/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # You should define a service name which is used as a cluster id by envoy. 3 | # envoy_service: 4 | envoy_hostname: "{{ inventory_hostname }}" 5 | envoy_admin_port: 8001 6 | envoy_image: "envoyproxy/envoy:ad81550d5f5026c33fcfc290a4f7afdc5e754dd0" 7 | envoy_data_dir: "/var/envoy/{{ envoy_service }}" 8 | envoy_vol: "{{ envoy_data_dir }}/vol" 9 | envoy_log_dir: "{{ envoy_data_dir }}/log" 10 | envoy_logleve: trace 11 | envoy_docker_runner: docker 12 | -------------------------------------------------------------------------------- /examples/ansible/roles/envoy/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # TODO: should we restart gracefully ? (or configure eds?) 3 | - name: Restart envoy 4 | command: docker-compose restart 5 | args: 6 | chdir: "{{ envoy_data_dir }}" 7 | become: yes 8 | become_user: "{{ envoy_docker_runner }}" 9 | -------------------------------------------------------------------------------- /examples/ansible/roles/envoy/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: mkdir envoy directory 3 | file: 4 | path: "{{ item }}" 5 | state: directory 6 | mode: 0777 7 | recurse: yes 8 | with_items: 9 | - "{{ envoy_data_dir }}" 10 | - "{{ envoy_vol }}" 11 | - "{{ envoy_log_dir }}" 12 | become: yes 13 | 14 | - name: Put envoy compose file 15 | template: 16 | src: docker-compose.yml.j2 17 | dest: "{{ envoy_data_dir }}/docker-compose.yml" 18 | 19 | - name: put envoy conf file 20 | template: 21 | src: envoy-conf.yaml.j2 22 | dest: "{{ envoy_vol }}/envoy.yaml" 23 | notify: Restart envoy 24 | 25 | # TODO: should reload gracefully? 26 | - name: Start envoy server 27 | command: docker-compose up -d 28 | args: 29 | chdir: "{{ envoy_data_dir }}" 30 | become: yes 31 | become_user: "{{ envoy_docker_runner }}" 32 | -------------------------------------------------------------------------------- /examples/ansible/roles/envoy/templates/docker-compose.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2' 3 | services: 4 | envoy-{{ envoy_service }}-{{ envoy_hostname }}: 5 | image: {{ envoy_image }} 6 | command: /usr/local/bin/envoy -c /var/envoy/envoy.yaml --service-cluster {{ envoy_service }} --service-node {{ envoy_hostname }} -l {{ envoy_logleve }} 7 | volumes: 8 | - {{ envoy_vol }}:/var/envoy 9 | - {{ envoy_log_dir }}:/var/log/envoy 10 | network_mode: "host" 11 | restart: always 12 | -------------------------------------------------------------------------------- /examples/ansible/roles/envoy/templates/envoy-conf.yaml.j2: -------------------------------------------------------------------------------- 1 | admin: 2 | access_log_path: "/var/envoy/admin.log" 3 | address: 4 | socket_address: 5 | address: 0.0.0.0 6 | port_value: {{ envoy_admin_port }} 7 | 8 | dynamic_resources: 9 | lds_config: 10 | api_config_source: 11 | api_type: GRPC 12 | cluster_names: [xds_cluster] 13 | cds_config: 14 | api_config_source: 15 | api_type: GRPC 16 | cluster_names: [xds_cluster] 17 | 18 | static_resources: 19 | clusters: 20 | - name: xds_cluster 21 | type: "{{ meshem_xds_endpoint.type }}" 22 | connect_timeout: 1s 23 | http2_protocol_options: {} 24 | hosts: 25 | {% for host in meshem_xds_endpoint.endpoints %} 26 | - socket_address: 27 | address: "{{ host.host }}" 28 | port_value: {{ host.port }} 29 | {% endfor %} 30 | {% if zipkin_endpoint is defined %} 31 | - name: zipkin 32 | connect_timeout: 1s 33 | type: "{{ zipkin_endpoint.type }}" 34 | lb_policy: round_robin 35 | hosts: 36 | {% for host in zipkin_endpoint.endpoints %} 37 | - socket_address: 38 | address: "{{ host.host }}" 39 | port_value: {{ host.port }} 40 | {% endfor %} 41 | {% endif %} 42 | {% if zipkin_endpoint is defined %} 43 | tracing: 44 | http: 45 | name: envoy.zipkin 46 | config: 47 | collector_cluster: zipkin 48 | collector_endpoint: "/api/v1/spans" 49 | {% endif %} 50 | 51 | -------------------------------------------------------------------------------- /examples/ansible/roles/front/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart front 3 | systemd: 4 | name: front.service 5 | state: restarted 6 | daemon_reload: yes 7 | become: yes 8 | -------------------------------------------------------------------------------- /examples/ansible/roles/front/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: go 4 | -------------------------------------------------------------------------------- /examples/ansible/roles/front/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Put Go source 3 | template: 4 | src: front.go.j2 5 | dest: /var/front.go 6 | become: yes 7 | notify: restart front 8 | 9 | - name: setup systemd 10 | template: 11 | src: front.service.j2 12 | dest: /etc/systemd/system/front.service 13 | owner: root 14 | group: root 15 | mode: 0644 16 | become: yes 17 | notify: restart front 18 | 19 | - name: starts front 20 | systemd: 21 | name: front.service 22 | state: started 23 | enabled: yes 24 | daemon_reload: yes 25 | become: yes 26 | -------------------------------------------------------------------------------- /examples/ansible/roles/front/templates/front.go.j2: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | var ( 12 | port = flag.Int("port", 8080, "listen port") 13 | apphost = flag.String("app", "", "app host") 14 | client = http.Client{} 15 | traceHeaders = []string{ 16 | "X-Ot-Span-Context", 17 | "X-Request-Id", 18 | "X-B3-TraceId", 19 | "X-B3-SpanId", 20 | "X-B3-ParentSpanId", 21 | "X-B3-Sampled", 22 | "X-B3-Flags", 23 | } 24 | ) 25 | 26 | func propagate(in *http.Request, out *http.Request) { 27 | for _, key := range traceHeaders { 28 | value := in.Header.Get(key) 29 | if value != "" { 30 | out.Header.Add(key, value) 31 | } 32 | } 33 | } 34 | 35 | func handler(w http.ResponseWriter, r *http.Request) { 36 | req, err := http.NewRequest("GET", *apphost, nil) 37 | if err != nil { 38 | fmt.Fprintf(w, "Error!!! %s\n", err.Error()) 39 | } 40 | 41 | // copy headers for tracing 42 | propagate(r, req) 43 | 44 | resp, err := client.Do(req) 45 | if err != nil { 46 | fmt.Fprintf(w, "Error!!! %s\n", err.Error()) 47 | } 48 | fmt.Println(resp) 49 | defer resp.Body.Close() 50 | 51 | body, _ := ioutil.ReadAll(resp.Body) 52 | fmt.Fprintf(w, "front: app response = %s\n", string(body)) 53 | } 54 | 55 | func newLogHandler(handler http.Handler) http.Handler { 56 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 57 | log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL) 58 | handler.ServeHTTP(w, r) 59 | }) 60 | } 61 | 62 | func main() { 63 | flag.Parse() 64 | http.HandleFunc("/", handler) 65 | err := http.ListenAndServe(fmt.Sprintf(":%d", *port), newLogHandler(http.DefaultServeMux)) 66 | if err != nil { 67 | println(err.Error) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/ansible/roles/front/templates/front.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=products 3 | Requires=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | Restart=on-failure 8 | ExecStart={{ go_root }}/bin/go run /var/front.go --port={{ front_port }} --app={{ front_app_endpoint }} 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | 13 | -------------------------------------------------------------------------------- /examples/ansible/roles/go/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | go_path: "~/go" 3 | go_home: /usr/local 4 | go_root: "{{ go_home }}/go" 5 | -------------------------------------------------------------------------------- /examples/ansible/roles/go/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create GOPATH directory 3 | file: 4 | path: "{{ go_path }}" 5 | state: directory 6 | mode: 0755 7 | become: yes 8 | 9 | - name: Check if go is installed 10 | command: "{{ go_root }}/bin/go version" 11 | register: go_version 12 | ignore_errors: yes 13 | 14 | - block: 15 | - name: Get go archive 16 | get_url: 17 | url: https://dl.google.com/go/go1.10.linux-amd64.tar.gz 18 | dest: /tmp/golang.tar.gz 19 | become: yes 20 | 21 | - name: Unarchive go package 22 | shell: "tar -C {{ go_home }} -xzf /tmp/golang.tar.gz" 23 | become: yes 24 | when: go_version|failed 25 | 26 | - name: go env 27 | template: 28 | src: go.sh.j2 29 | dest: /etc/profile.d/go.sh 30 | mode: 0644 31 | become: yes 32 | -------------------------------------------------------------------------------- /examples/ansible/roles/go/templates/go.sh.j2: -------------------------------------------------------------------------------- 1 | export GOROOT={{ go_root }} 2 | export GOPATH={{ go_path }} 3 | export GOBIN=$GOPATH/bin 4 | export PATH=$PATH:$GOROOT/bin:$GOBIN 5 | -------------------------------------------------------------------------------- /examples/ansible/site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: app 3 | roles: 4 | - app 5 | 6 | - hosts: front 7 | roles: 8 | - front 9 | -------------------------------------------------------------------------------- /examples/ansible/vagrant: -------------------------------------------------------------------------------- 1 | myfront ansible_host=192.168.34.70 2 | myapp1 ansible_host=192.168.34.71 3 | myapp2 ansible_host=192.168.34.72 4 | 5 | [app] 6 | myapp1 7 | myapp2 8 | 9 | [front] 10 | myfront 11 | 12 | [data-plane] 13 | myfront 14 | myapp1 15 | myapp2 16 | 17 | [vagrant:children] 18 | data-plane 19 | -------------------------------------------------------------------------------- /examples/docker/Dockerfile-client: -------------------------------------------------------------------------------- 1 | FROM golang:1.9 2 | 3 | RUN apt-get update && apt-get -q install -y \ 4 | curl netcat 5 | 6 | WORKDIR /go/src/github.com/rerorero/meshem 7 | 8 | CMD tail -f /dev/null 9 | 10 | -------------------------------------------------------------------------------- /examples/docker/Dockerfile-envoy: -------------------------------------------------------------------------------- 1 | FROM envoyproxy/envoy-alpine:latest 2 | 3 | RUN mkdir -p /var/log/envoy 4 | CMD /usr/local/bin/envoy -c /etc/envoy.yaml -l trace 5 | -------------------------------------------------------------------------------- /examples/docker/Dockerfile-meshem: -------------------------------------------------------------------------------- 1 | FROM golang:1.9 2 | EXPOSE 8090 3 | WORKDIR /go/src/github.com/rerorero/meshem 4 | CMD go run src/meshem/main.go --conf.file ./examples/docker/meshem.yaml 5 | -------------------------------------------------------------------------------- /examples/docker/app1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build-env 2 | 3 | ADD . /src 4 | RUN cd /src && go build -o app1 5 | 6 | FROM alpine 7 | WORKDIR /app 8 | COPY --from=build-env /src/app1 /app/ 9 | 10 | EXPOSE 9001 11 | CMD ./app1 12 | -------------------------------------------------------------------------------- /examples/docker/app1/app1-svc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | protocol: HTTP 3 | hosts: 4 | - name: app1-01 5 | ingressAddr: 6 | host: 10.2.1.11 7 | port: 80 8 | substanceAddr: 9 | host: 10.2.1.1 10 | port: 9001 11 | egressHost: 10.2.1.11 12 | - name: app1-02 13 | ingressAddr: 14 | host: 10.2.1.12 15 | port: 80 16 | substanceAddr: 17 | host: 10.2.1.2 18 | port: 9001 19 | egressHost: 10.2.1.12 20 | dependentServices: [] 21 | -------------------------------------------------------------------------------- /examples/docker/app1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | func handler(w http.ResponseWriter, r *http.Request) { 11 | fmt.Fprintf(w, "app1: %s\n", os.Getenv("message")) 12 | } 13 | 14 | func newLogHandler(handler http.Handler) http.Handler { 15 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | log.Printf("%s %s %s header=%+v", r.RemoteAddr, r.Method, r.URL, r.Header) 17 | handler.ServeHTTP(w, r) 18 | }) 19 | } 20 | 21 | func main() { 22 | http.HandleFunc("/", handler) 23 | http.ListenAndServe(":9001", newLogHandler(http.DefaultServeMux)) 24 | } 25 | -------------------------------------------------------------------------------- /examples/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | 5 | consul: 6 | image: consul:1.0.3 7 | environment: 8 | - CONSUL_BIND_INTERFACE=eth0 9 | - 'CONSUL_LOCAL_CONFIG={"acl_datacenter": "dc1", "acl_default_policy": "deny", "acl_master_token": "master"}' 10 | expose: [8500] 11 | ports: 12 | - 28500:8500 13 | networks: 14 | test: 15 | ipv4_address: 10.2.0.4 16 | 17 | meshem: 18 | build: 19 | context: . 20 | dockerfile: Dockerfile-meshem 21 | volumes: 22 | - ../../.:/go/src/github.com/rerorero/meshem 23 | expose: 24 | - 8090 25 | - 8091 26 | networks: 27 | test: 28 | ipv4_address: 10.2.0.2 29 | 30 | app1-01: 31 | build: 32 | context: ./app1 33 | dockerfile: Dockerfile 34 | environment: 35 | message: "respond from app1-01" 36 | expose: [9001] 37 | networks: 38 | test: 39 | ipv4_address: 10.2.1.1 40 | envoy-app1-01: 41 | build: 42 | context: . 43 | dockerfile: Dockerfile-envoy 44 | volumes: 45 | - ./envoy.yaml:/etc/envoy.yaml 46 | command: "/usr/local/bin/envoy -c /etc/envoy.yaml -l trace --service-cluster app1 --service-node app1-01" 47 | expose: 48 | - "80" 49 | - "8001" 50 | networks: 51 | test: 52 | ipv4_address: 10.2.1.11 53 | 54 | app1-02: 55 | build: 56 | context: ./app1 57 | dockerfile: Dockerfile 58 | environment: 59 | message: "respond from app1-02" 60 | expose: [9001] 61 | networks: 62 | test: 63 | ipv4_address: 10.2.1.2 64 | envoy-app1-02: 65 | build: 66 | context: . 67 | dockerfile: Dockerfile-envoy 68 | volumes: 69 | - ./envoy.yaml:/etc/envoy.yaml 70 | command: "/usr/local/bin/envoy -c /etc/envoy.yaml -l trace --service-cluster app1 --service-node app1-02" 71 | expose: 72 | - "80" 73 | - "8001" 74 | networks: 75 | test: 76 | ipv4_address: 10.2.1.12 77 | 78 | front: 79 | build: 80 | context: ./front 81 | dockerfile: Dockerfile 82 | environment: 83 | egress_app1: "http://10.2.2.11:9000" 84 | expose: [8080] 85 | networks: 86 | test: 87 | ipv4_address: 10.2.2.1 88 | envoy-front: 89 | build: 90 | context: . 91 | dockerfile: Dockerfile-envoy 92 | volumes: 93 | - ./envoy.yaml:/etc/envoy.yaml 94 | command: "/usr/local/bin/envoy -c /etc/envoy.yaml -l trace --service-cluster front --service-node front" 95 | expose: 96 | - "80" 97 | - "9000" 98 | - "8001" 99 | networks: 100 | test: 101 | ipv4_address: 10.2.2.11 102 | 103 | zipkin: 104 | image: openzipkin/zipkin 105 | expose: 106 | - "9411" 107 | ports: 108 | - "19411:9411" 109 | networks: 110 | test: 111 | ipv4_address: 10.2.3.1 112 | 113 | client: 114 | build: 115 | context: . 116 | dockerfile: Dockerfile-client 117 | volumes: 118 | - ../../.:/go/src/github.com/rerorero/meshem 119 | networks: 120 | test: 121 | ipv4_address: 10.2.0.3 122 | 123 | networks: 124 | test: 125 | driver: bridge 126 | ipam: 127 | config: 128 | - subnet: 10.2.0.0/16 129 | gateway: 10.2.0.1 130 | -------------------------------------------------------------------------------- /examples/docker/envoy.yaml: -------------------------------------------------------------------------------- 1 | admin: 2 | access_log_path: "/var/log/envoy-admin.log" 3 | address: 4 | socket_address: 5 | address: 0.0.0.0 6 | port_value: 8001 7 | 8 | dynamic_resources: 9 | lds_config: 10 | api_config_source: 11 | api_type: GRPC 12 | cluster_names: [xds_cluster] 13 | cds_config: 14 | api_config_source: 15 | api_type: GRPC 16 | cluster_names: [xds_cluster] 17 | 18 | static_resources: 19 | clusters: 20 | - name: xds_cluster 21 | type: STATIC 22 | connect_timeout: 10s 23 | http2_protocol_options: {} 24 | hosts: 25 | - socket_address: 26 | address: 10.2.0.2 27 | port_value: 8090 28 | - name: zipkin 29 | connect_timeout: 1s 30 | type: static 31 | lb_policy: round_robin 32 | hosts: 33 | - socket_address: 34 | address: 10.2.3.1 35 | port_value: 9411 36 | tracing: 37 | http: 38 | name: envoy.zipkin 39 | config: 40 | collector_cluster: zipkin 41 | collector_endpoint: "/api/v1/spans" 42 | -------------------------------------------------------------------------------- /examples/docker/front/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build-env 2 | 3 | ADD . /src 4 | RUN cd /src && go build -o front 5 | 6 | FROM alpine 7 | WORKDIR /front 8 | COPY --from=build-env /src/front /front/ 9 | 10 | EXPOSE 8080 11 | CMD ./front 12 | -------------------------------------------------------------------------------- /examples/docker/front/front-svc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | protocol: HTTP 3 | hosts: 4 | - name: front 5 | ingressAddr: 6 | host: 10.2.2.11 7 | port: 8088 8 | substanceAddr: 9 | host: 10.2.2.1 10 | port: 8080 11 | egressHost: 10.2.2.11 12 | dependentServices: 13 | - name: app1 14 | egressPort: 9000 15 | -------------------------------------------------------------------------------- /examples/docker/front/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | var ( 12 | client = http.Client{} 13 | traceHeaders = []string{ 14 | "X-Ot-Span-Context", 15 | "X-Request-Id", 16 | "X-B3-TraceId", 17 | "X-B3-SpanId", 18 | "X-B3-ParentSpanId", 19 | "X-B3-Sampled", 20 | "X-B3-Flags", 21 | } 22 | ) 23 | 24 | func propagate(in *http.Request, out *http.Request) { 25 | for _, key := range traceHeaders { 26 | value := in.Header.Get(key) 27 | if value != "" { 28 | out.Header.Add(key, value) 29 | } 30 | } 31 | } 32 | 33 | func handler(w http.ResponseWriter, r *http.Request) { 34 | appHost := os.Getenv("egress_app1") 35 | req, err := http.NewRequest("GET", appHost, nil) 36 | if err != nil { 37 | fmt.Fprintf(w, "Error!!! %s\n", err.Error()) 38 | } 39 | 40 | // copy headers for tracing 41 | propagate(r, req) 42 | 43 | resp, err := client.Do(req) 44 | if err != nil { 45 | fmt.Fprintf(w, "Error!!! %s\n", err.Error()) 46 | } 47 | defer resp.Body.Close() 48 | 49 | body, _ := ioutil.ReadAll(resp.Body) 50 | fmt.Fprintf(w, "front: app1 response = %s\n", string(body)) 51 | } 52 | 53 | func newLogHandler(handler http.Handler) http.Handler { 54 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | log.Printf("%s %s %s header=%+v", r.RemoteAddr, r.Method, r.URL, r.Header) 56 | handler.ServeHTTP(w, r) 57 | }) 58 | } 59 | 60 | func main() { 61 | http.HandleFunc("/", handler) 62 | http.ListenAndServe(":8080", newLogHandler(http.DefaultServeMux)) 63 | } 64 | -------------------------------------------------------------------------------- /examples/docker/meshem.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | xds: 3 | port: 8090 4 | consul: 5 | url: "http://10.2.0.4:8500" 6 | token: master 7 | datacenter: dc1 8 | ctlapi: 9 | port: 8091 10 | discovery: 11 | type: consul 12 | consul: 13 | url: "http://10.2.0.4:8500" 14 | token: master 15 | datacenter: dc1 16 | -------------------------------------------------------------------------------- /examples/docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | docker-compose down 6 | docker-compose up --build -d 7 | 8 | sleep 5 9 | 10 | docker exec docker_client_1 /bin/bash -c " 11 | set -eux 12 | 13 | export MESHEM_CTLAPI_ENDPOINT=http://10.2.0.2:8091 14 | while ! nc -z 10.2.0.2 8091; do 15 | sleep 1 16 | done 17 | 18 | go run src/meshemctl/main.go svc apply app1 -f examples/docker/app1/app1-svc.yml 19 | go run src/meshemctl/main.go svc apply front -f examples/docker/front/front-svc.yml 20 | curl 'http://10.2.0.4:8500/v1/kv/hosts?token=master&keys=true' 21 | curl 'http://10.2.0.4:8500/v1/catalog/nodes?token=master' 22 | " 23 | -------------------------------------------------------------------------------- /images/control-plane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rerorero/meshem/0178fcabc661060b63ff5b5ac02a5750611aa704/images/control-plane.png -------------------------------------------------------------------------------- /images/data-plane1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rerorero/meshem/0178fcabc661060b63ff5b5ac02a5750611aa704/images/data-plane1.png -------------------------------------------------------------------------------- /images/data-plane2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rerorero/meshem/0178fcabc661060b63ff5b5ac02a5750611aa704/images/data-plane2.png -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux 3 | image="$1" 4 | tag="$2" 5 | 6 | # build 7 | docker build -t $image:$tag ./ 8 | docker tag $image:$tag $image:latest 9 | # push 10 | docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD"; 11 | docker push $image:$tag 12 | docker push $image:latest 13 | 14 | # build binaries 15 | $GOPATH/bin/gox -os="linux darwin windows" -arch="amd64" github.com/rerorero/meshem/src/meshem github.com/rerorero/meshem/src/meshemctl 16 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux 3 | 4 | go get github.com/golang/dep/cmd/dep 5 | dep ensure 6 | go install ./src/meshemctl 7 | 8 | # create VMs 9 | vagrant up 10 | 11 | # provision control-plane 12 | pushd ./ansible 13 | pip install -r requirements.txt 14 | ansible-playbook -i inventories/vagrant site.yml 15 | popd 16 | 17 | # provision example services with envoy proxy 18 | pushd ./examples/ansible 19 | pip install -r requirements.txt 20 | ansible-playbook -i vagrant data-plane.yml 21 | export MESHEM_CTLAPI_ENDPOINT="http://192.168.34.61:8091" 22 | meshemctl svc apply app -f ./meshem-conf/app.yaml 23 | meshemctl svc apply front -f ./meshem-conf/front.yaml 24 | ansible-playbook -i vagrant site.yml -e "front_app_endpoint=http://127.0.0.1:9001/" 25 | popd 26 | -------------------------------------------------------------------------------- /src/core/ctlapi/client.go: -------------------------------------------------------------------------------- 1 | package ctlapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // APIClient is client for ctlapi. 15 | type APIClient struct { 16 | endpoint *url.URL 17 | client http.Client 18 | } 19 | 20 | // NewClient returns a new ApiClient. 21 | func NewClient(endpoint string, timeout time.Duration) (*APIClient, error) { 22 | client := http.Client{ 23 | Timeout: timeout, 24 | } 25 | url, err := url.Parse(endpoint) 26 | if err != nil { 27 | return nil, errors.Wrapf(err, "invalid API endpoint: %s", endpoint) 28 | } 29 | return &APIClient{ 30 | endpoint: url, 31 | client: client, 32 | }, nil 33 | } 34 | 35 | // Post requests a POST method. 36 | func (client *APIClient) Post(url string, body interface{}) (int, []byte, error) { 37 | return client.request(url, http.MethodPost, body) 38 | } 39 | 40 | // Put requests a PUT method. 41 | func (client *APIClient) Put(url string, body interface{}) (int, []byte, error) { 42 | return client.request(url, http.MethodPut, body) 43 | } 44 | 45 | // Get requests a PUT method. 46 | func (client *APIClient) Get(url string) (int, []byte, error) { 47 | return client.request(url, http.MethodGet, nil) 48 | } 49 | 50 | // Delete requests a DELETE method. 51 | func (client *APIClient) Delete(url string) (int, []byte, error) { 52 | return client.request(url, http.MethodDelete, nil) 53 | } 54 | 55 | func (client *APIClient) request(url string, method string, body interface{}) (int, []byte, error) { 56 | var byte []byte 57 | var err error 58 | if body != nil { 59 | byte, err = json.Marshal(body) 60 | if err != nil { 61 | return 0, nil, err 62 | } 63 | } 64 | req, err := http.NewRequest(method, url, bytes.NewBuffer(byte)) 65 | if err != nil { 66 | return 0, nil, err 67 | } 68 | res, err := client.client.Do(req) 69 | if err != nil { 70 | return 0, nil, err 71 | } 72 | resBody, err := ioutil.ReadAll(res.Body) 73 | if err != nil { 74 | return 0, nil, err 75 | } 76 | return res.StatusCode, resBody, err 77 | } 78 | -------------------------------------------------------------------------------- /src/core/ctlapi/rest_services.go: -------------------------------------------------------------------------------- 1 | package ctlapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/julienschmidt/httprouter" 9 | "github.com/rerorero/meshem/src/model" 10 | ) 11 | 12 | // PostServiceReq is the request type of POST service method. 13 | type PostServiceReq struct { 14 | Protocol string `json:"protocol"` 15 | } 16 | 17 | // PutServiceResp is theresponse type of PUT service method. 18 | type PutServiceResp struct { 19 | Changed bool `json:"changed"` 20 | } 21 | 22 | // postService is handler to create a new service. 23 | func (srv *Server) postSerivce(w http.ResponseWriter, r *http.Request, param httprouter.Params, body []byte) { 24 | var req PostServiceReq 25 | if err := json.Unmarshal(body, &req); err != nil { 26 | srv.respondError(http.StatusBadRequest, w, err) 27 | return 28 | } 29 | 30 | service, err := srv.inventory.RegisterService(param.ByName("name"), req.Protocol) 31 | if err != nil { 32 | srv.respondError(http.StatusInternalServerError, w, err) 33 | return 34 | } 35 | 36 | srv.respondJson(http.StatusCreated, w, &service) 37 | } 38 | 39 | // PostService calls POST service. 40 | func (client *APIClient) PostService(name string, req PostServiceReq) (service model.Service, status int, err error) { 41 | var body []byte 42 | status, body, err = client.Post(client.serviceURIof(name), req) 43 | if err != nil { 44 | return service, status, err 45 | } 46 | err = json.Unmarshal(body, &service) 47 | return service, status, err 48 | } 49 | 50 | // getSerivce is handler to get a service. 51 | func (srv *Server) getSerivce(w http.ResponseWriter, r *http.Request, param httprouter.Params, _ []byte) { 52 | service, ok, err := srv.inventory.GetService(param.ByName("name")) 53 | if err != nil { 54 | srv.respondError(http.StatusInternalServerError, w, err) 55 | return 56 | } 57 | if !ok { 58 | srv.respondError(http.StatusNotFound, w, fmt.Errorf("not found")) 59 | return 60 | } 61 | 62 | hosts, err := srv.inventory.GetHostsOfService(service.Name) 63 | if err != nil { 64 | srv.respondError(http.StatusInternalServerError, w, err) 65 | return 66 | } 67 | res := model.NewIdempotentService(&service, hosts) 68 | srv.respondJson(http.StatusOK, w, res) 69 | } 70 | 71 | // GetService calls GET service. 72 | func (client *APIClient) GetService(name string) (resp model.IdempotentServiceParam, status int, err error) { 73 | var body []byte 74 | status, body, err = client.Get(client.serviceURIof(name)) 75 | if err != nil { 76 | return resp, status, err 77 | } 78 | err = json.Unmarshal(body, &resp) 79 | return resp, status, err 80 | } 81 | 82 | // putService is handler to create/update a service idempotently. 83 | func (srv *Server) putSerivce(w http.ResponseWriter, r *http.Request, param httprouter.Params, body []byte) { 84 | var req model.IdempotentServiceParam 85 | if err := json.Unmarshal(body, &req); err != nil { 86 | srv.respondError(http.StatusBadRequest, w, err) 87 | return 88 | } 89 | 90 | changed, err := srv.inventory.IdempotentService(param.ByName("name"), req) 91 | if err != nil { 92 | srv.respondError(http.StatusInternalServerError, w, err) 93 | return 94 | } 95 | 96 | res := PutServiceResp{Changed: changed} 97 | srv.respondJson(http.StatusOK, w, &res) 98 | } 99 | 100 | // PutService calls PUT service. 101 | func (client *APIClient) PutService(name string, req model.IdempotentServiceParam) (resp PutServiceResp, status int, err error) { 102 | var body []byte 103 | status, body, err = client.Put(client.serviceURIof(name), req) 104 | if err != nil { 105 | return resp, status, err 106 | } 107 | err = json.Unmarshal(body, &resp) 108 | return resp, status, err 109 | } 110 | 111 | func (client *APIClient) serviceURIof(name string) string { 112 | return fmt.Sprintf("%s/%s/%s", client.endpoint.String(), ServiceURI, name) 113 | } 114 | -------------------------------------------------------------------------------- /src/core/ctlapi/rest_services_test.go: -------------------------------------------------------------------------------- 1 | package ctlapi 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | 10 | "github.com/rerorero/meshem/src/model" 11 | "github.com/sirupsen/logrus" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/mock" 14 | ) 15 | 16 | type MockedInventory struct { 17 | mock.Mock 18 | } 19 | 20 | func (i *MockedInventory) RegisterService(name string, protocol string) (model.Service, error) { 21 | args := i.Called(name, protocol) 22 | return args.Get(0).(model.Service), args.Error(1) 23 | } 24 | func (i *MockedInventory) UnregisterService(name string) (deleted bool, referer []string, err error) { 25 | args := i.Called(name) 26 | return args.Bool(0), args.Get(1).([]string), args.Error(2) 27 | } 28 | func (i *MockedInventory) GetService(name string) (model.Service, bool, error) { 29 | args := i.Called(name) 30 | return args.Get(0).(model.Service), args.Bool(1), args.Error(2) 31 | } 32 | func (i *MockedInventory) GetServiceNames() ([]string, error) { 33 | args := i.Called() 34 | return args.Get(0).([]string), args.Error(1) 35 | } 36 | func (i *MockedInventory) AddServiceDependency(serviceName string, dependServiceNames string, egressPort uint32) error { 37 | args := i.Called(serviceName, dependServiceNames, egressPort) 38 | return args.Error(0) 39 | } 40 | func (i *MockedInventory) RemoveServiceDependency(serviceName string, dependServiceNames string) (bool, error) { 41 | args := i.Called(serviceName, dependServiceNames) 42 | return args.Bool(0), args.Error(1) 43 | } 44 | func (i *MockedInventory) GetRefferersOf(serviceName string) ([]string, error) { 45 | args := i.Called(serviceName) 46 | return args.Get(0).([]string), args.Error(1) 47 | } 48 | func (i *MockedInventory) RegisterHost(serviceName, hostName, ingressAddr, substanceAddr, egressHost string) (model.Host, error) { 49 | args := i.Called(serviceName, hostName, ingressAddr, substanceAddr, egressHost) 50 | return args.Get(0).(model.Host), args.Error(1) 51 | } 52 | func (i *MockedInventory) UnregisterHost(serviceName string, hostName string) (bool, error) { 53 | args := i.Called(serviceName, hostName) 54 | return args.Bool(0), args.Error(1) 55 | } 56 | func (i *MockedInventory) GetHostByName(name string) (model.Host, bool, error) { 57 | args := i.Called(name) 58 | return args.Get(0).(model.Host), args.Bool(1), args.Error(2) 59 | } 60 | func (i *MockedInventory) GetHostNames() ([]string, error) { 61 | args := i.Called() 62 | return args.Get(0).([]string), args.Error(1) 63 | } 64 | func (i *MockedInventory) GetHostsOfService(serviceName string) ([]model.Host, error) { 65 | args := i.Called(serviceName) 66 | return args.Get(0).([]model.Host), args.Error(1) 67 | } 68 | func (i *MockedInventory) UpdateHost(serviceName string, hostName string, ingressAddr, substanceAddr, egressHost *string) (host model.Host, err error) { 69 | args := i.Called(serviceName, hostName, ingressAddr, substanceAddr, egressHost) 70 | return args.Get(0).(model.Host), args.Error(1) 71 | } 72 | func (i *MockedInventory) IdempotentService(serviceName string, param model.IdempotentServiceParam) (changed bool, err error) { 73 | args := i.Called(serviceName, param) 74 | return args.Bool(0), args.Error(1) 75 | } 76 | 77 | func TestPostService(t *testing.T) { 78 | inventory := MockedInventory{} 79 | server := NewServer(&inventory, model.CtlAPIConf{}, logrus.New()) 80 | sut := httptest.NewServer(server) 81 | defer sut.Close() 82 | client, _ := NewClient(sut.URL, 60*time.Second) 83 | 84 | expect := model.Service{ 85 | Name: "ban", 86 | Protocol: "HTTP", 87 | } 88 | inventory.On("RegisterService", "test1", "HTTP").Return(expect, nil) 89 | 90 | actual, status, err := client.PostService("test1", PostServiceReq{Protocol: "HTTP"}) 91 | assert.NoError(t, err) 92 | assert.Equal(t, http.StatusCreated, status) 93 | assert.Equal(t, expect, actual) 94 | 95 | // error 96 | inventory.On("RegisterService", "test2", "unknown").Return(expect, errors.New("something happens")) 97 | actual, status, err = client.PostService("test2", PostServiceReq{Protocol: "unknown"}) 98 | assert.NoError(t, err) 99 | assert.Equal(t, http.StatusInternalServerError, status) 100 | } 101 | 102 | func TestGetService(t *testing.T) { 103 | inventory := MockedInventory{} 104 | server := NewServer(&inventory, model.CtlAPIConf{}, logrus.New()) 105 | sut := httptest.NewServer(server) 106 | defer sut.Close() 107 | client, _ := NewClient(sut.URL, 60*time.Second) 108 | 109 | expect := model.IdempotentServiceParam{ 110 | Protocol: "HTTP", 111 | Hosts: []model.Host{{ 112 | Name: "host1", 113 | IngressAddr: model.Address{Hostname: "host1", Port: 1234}, 114 | SubstanceAddr: model.Address{Hostname: "127.0.0.1", Port: 5678}, 115 | EgressHost: "hostname", 116 | }}, 117 | } 118 | expectsvc := expect.NewService("test1") 119 | inventory.On("GetService", "test1").Return(expectsvc, true, nil) 120 | inventory.On("GetHostsOfService", "test1").Return(expect.Hosts, nil) 121 | 122 | actual, status, err := client.GetService("test1") 123 | assert.NoError(t, err) 124 | assert.Equal(t, http.StatusOK, status) 125 | assert.Equal(t, expect, actual) 126 | 127 | // error 128 | inventory.On("GetService", "test2").Return(expectsvc, false, nil) 129 | actual, status, err = client.GetService("test2") 130 | assert.NoError(t, err) 131 | assert.Equal(t, http.StatusNotFound, status) 132 | } 133 | 134 | func TestIdempotentService(t *testing.T) { 135 | inventory := MockedInventory{} 136 | server := NewServer(&inventory, model.CtlAPIConf{}, logrus.New()) 137 | sut := httptest.NewServer(server) 138 | defer sut.Close() 139 | client, _ := NewClient(sut.URL, 60*time.Second) 140 | 141 | param := model.IdempotentServiceParam{ 142 | Protocol: "TCP", 143 | DependentServices: []model.DependentService{ 144 | { 145 | Name: "svcC", 146 | EgressPort: 9004, 147 | }, 148 | }, 149 | Hosts: []model.Host{ 150 | { 151 | Name: "b-1", 152 | IngressAddr: model.Address{Hostname: "192.168.0.2", Port: 9000}, 153 | SubstanceAddr: model.Address{Hostname: "127.0.0.1", Port: 9001}, 154 | EgressHost: "127.0.0.1", 155 | }, 156 | }, 157 | } 158 | inventory.On("IdempotentService", "test1", param).Return(true, nil) 159 | 160 | actual, status, err := client.PutService("test1", param) 161 | assert.NoError(t, err) 162 | assert.Equal(t, http.StatusOK, status) 163 | assert.Equal(t, PutServiceResp{Changed: true}, actual) 164 | 165 | // error 166 | inventory.On("IdempotentService", "test2", param).Return(false, errors.New("error")) 167 | actual, status, err = client.PutService("test2", param) 168 | assert.NoError(t, err) 169 | assert.Equal(t, http.StatusInternalServerError, status) 170 | } 171 | -------------------------------------------------------------------------------- /src/core/ctlapi/server.go: -------------------------------------------------------------------------------- 1 | package ctlapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | "github.com/julienschmidt/httprouter" 11 | "github.com/pkg/errors" 12 | "github.com/rerorero/meshem/src/core" 13 | "github.com/rerorero/meshem/src/model" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // Server is a API server. 18 | type Server struct { 19 | inventory core.InventoryService 20 | router *httprouter.Router 21 | conf model.CtlAPIConf 22 | logger *logrus.Logger 23 | bodyMaxbyteLen int64 24 | } 25 | 26 | // APIHandler is common HTTP handler type. 27 | type APIHandler func(w http.ResponseWriter, r *http.Request, param httprouter.Params, body []byte) 28 | 29 | type errorRes struct { 30 | Error string `json:"error"` 31 | } 32 | 33 | const ( 34 | // ServiceURI is uri prefix for service resources. 35 | ServiceURI = "services" 36 | ) 37 | 38 | // NewServer creates a new API server. 39 | func NewServer(inventory core.InventoryService, conf model.CtlAPIConf, logger *logrus.Logger) *Server { 40 | srv := &Server{ 41 | inventory: inventory, 42 | router: httprouter.New(), 43 | conf: conf, 44 | logger: logger, 45 | bodyMaxbyteLen: 1024 * 1024, 46 | } 47 | srv.router.POST(fmt.Sprintf("/%s/:name/", ServiceURI), srv.handlerOf(srv.postSerivce)) 48 | srv.router.GET(fmt.Sprintf("/%s/:name/", ServiceURI), srv.handlerOf(srv.getSerivce)) 49 | srv.router.PUT(fmt.Sprintf("/%s/:name/", ServiceURI), srv.handlerOf(srv.putSerivce)) 50 | return srv 51 | } 52 | 53 | func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 54 | srv.router.ServeHTTP(w, r) 55 | } 56 | 57 | // Run run 58 | func (srv *Server) Run() error { 59 | go func() { 60 | srv.logger.Infof("ctlapi server listening on %d", srv.conf.Port) 61 | err := http.ListenAndServe(fmt.Sprintf(":%d", srv.conf.Port), srv) 62 | if err != nil { 63 | srv.logger.Error("failed to start api server") 64 | srv.logger.Error(err) 65 | } 66 | srv.logger.Info("ctlapi server shutdown") 67 | }() 68 | return nil 69 | } 70 | 71 | func (srv *Server) respondJson(code int, w http.ResponseWriter, body interface{}) { 72 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 73 | w.WriteHeader(code) 74 | if err := json.NewEncoder(w).Encode(body); err != nil { 75 | srv.logger.Errorf("ctlapi encode failed: %+v", body) 76 | } 77 | } 78 | 79 | func (srv *Server) respondError(code int, w http.ResponseWriter, err error) { 80 | srv.logger.Errorf("ctlapi error occurs(%d): %v", code, err) 81 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 82 | w.WriteHeader(code) 83 | res := errorRes{Error: err.Error()} 84 | if err := json.NewEncoder(w).Encode(res); err != nil { 85 | srv.logger.Errorf("ctlapi encode failed") 86 | } 87 | } 88 | 89 | func (srv *Server) authenticate(r *http.Request) error { 90 | // TODO 91 | return nil 92 | } 93 | 94 | // handlerOf is common handler process. 95 | func (srv *Server) handlerOf(h APIHandler) httprouter.Handle { 96 | return func(w http.ResponseWriter, r *http.Request, param httprouter.Params) { 97 | err := srv.authenticate(r) 98 | if err != nil { 99 | srv.respondError(http.StatusForbidden, w, err) 100 | return 101 | } 102 | body, err := ioutil.ReadAll(io.LimitReader(r.Body, srv.bodyMaxbyteLen)) 103 | if err != nil { 104 | srv.respondError(http.StatusInternalServerError, w, errors.Wrap(err, "failed to read body")) 105 | } 106 | h(w, r, param, body) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/core/inventory.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/rerorero/meshem/src/model" 9 | "github.com/rerorero/meshem/src/repository" 10 | "github.com/rerorero/meshem/src/utils" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // InventoryService is domain service shich manages meshem inventories. 15 | type InventoryService interface { 16 | RegisterService(name string, protocol string) (model.Service, error) 17 | UnregisterService(name string) (deleted bool, referer []string, err error) 18 | GetService(name string) (model.Service, bool, error) 19 | GetServiceNames() ([]string, error) 20 | AddServiceDependency(serviceName string, dependServiceNames string, egressPort uint32) error 21 | RemoveServiceDependency(serviceName string, dependServiceNames string) (bool, error) 22 | GetRefferersOf(serviceName string) ([]string, error) 23 | RegisterHost(serviceName, hostName, ingressAddr, substanceAddr, egressHost string) (model.Host, error) 24 | UnregisterHost(serviceName string, hostName string) (bool, error) 25 | GetHostByName(name string) (model.Host, bool, error) 26 | GetHostNames() ([]string, error) 27 | GetHostsOfService(serviceName string) ([]model.Host, error) 28 | UpdateHost(serviceName string, hostName string, ingressAddr, substanceAddr, egressHost *string) (host model.Host, err error) 29 | IdempotentService(serviceName string, param model.IdempotentServiceParam) (changed bool, err error) 30 | } 31 | 32 | type inventoryService struct { 33 | repo repository.InventoryRepository 34 | discovery repository.DiscoveryRepository 35 | versionGen VersionGenerator 36 | logger *logrus.Logger 37 | } 38 | 39 | // NewInventoryService creates an InventoryService instance. 40 | func NewInventoryService( 41 | repo repository.InventoryRepository, 42 | discoery repository.DiscoveryRepository, 43 | versionGen VersionGenerator, 44 | logger *logrus.Logger, 45 | ) InventoryService { 46 | return &inventoryService{ 47 | repo: repo, 48 | discovery: discoery, 49 | versionGen: versionGen, 50 | logger: logger, 51 | } 52 | } 53 | 54 | // RegisterService stores a new Service object. 55 | // TODO: be with consistency 56 | func (inv *inventoryService) RegisterService(name string, protocol string) (service model.Service, err error) { 57 | service = model.NewService(name, protocol) 58 | 59 | err = service.Validate() 60 | if err != nil { 61 | return service, err 62 | } 63 | 64 | // check dup 65 | names, err := inv.repo.SelectAllServiceNames() 66 | if err != nil { 67 | return service, err 68 | } 69 | _, ok := utils.ContainsString(names, name) 70 | if ok { 71 | return service, fmt.Errorf("service %s already exists", name) 72 | } 73 | 74 | version := inv.versionGen.New() 75 | err = inv.repo.PutService(service, version) 76 | if err != nil { 77 | return service, err 78 | } 79 | service.Version = version 80 | 81 | inv.logger.Infof("Service %s is registered! version=%s", name, service.Version) 82 | 83 | return service, nil 84 | } 85 | 86 | // UnregisterService removes the Service object and removes dependencies of all service. 87 | func (inv *inventoryService) UnregisterService(name string) (deleted bool, referrers []string, err error) { 88 | referrers, err = inv.repo.SelectReferringServiceNamesTo(name) 89 | if err != nil { 90 | return false, nil, err 91 | } 92 | 93 | for _, ref := range referrers { 94 | _, err := inv.RemoveServiceDependency(ref, name) 95 | if err != nil { 96 | return false, referrers, err 97 | } 98 | } 99 | 100 | // delete service object 101 | deleted, err = inv.repo.DeleteService(name) 102 | if err != nil { 103 | return false, referrers, err 104 | } 105 | 106 | if deleted { 107 | inv.logger.Infof("Service %s is deleted!", name) 108 | } 109 | return deleted, referrers, nil 110 | } 111 | 112 | // GetServiceRelations returns names of service which refferes the service. 113 | func (inv *inventoryService) GetRefferersOf(serviceName string) ([]string, error) { 114 | return inv.repo.SelectReferringServiceNamesTo(serviceName) 115 | } 116 | 117 | // GetService finds a service by name. 118 | func (inv *inventoryService) GetService(name string) (model.Service, bool, error) { 119 | return inv.repo.SelectServiceByName(name) 120 | } 121 | 122 | // GetServiceNames returns all service names 123 | func (inv *inventoryService) GetServiceNames() ([]string, error) { 124 | return inv.repo.SelectAllServiceNames() 125 | } 126 | 127 | // AddServiceDependency adds a new service dependency to the service. 128 | func (inv *inventoryService) AddServiceDependency(serviceName string, dependServiceName string, egressPort uint32) error { 129 | hosts, err := inv.GetHostsOfService(serviceName) 130 | if err != nil { 131 | return nil 132 | } 133 | 134 | var i int 135 | for i = 0; i < len(hosts); i++ { 136 | if hosts[i].IngressAddr.Port == egressPort { 137 | return fmt.Errorf("port=%d is already used by the ingress of %s", egressPort, hosts[i].Name) 138 | } 139 | if hosts[i].SubstanceAddr.Port == egressPort { 140 | return fmt.Errorf("port=%d is already used by the substance of %s", egressPort, hosts[i].Name) 141 | } 142 | } 143 | 144 | depend := model.DependentService{Name: dependServiceName, EgressPort: egressPort} 145 | version := inv.versionGen.New() 146 | err = inv.repo.AddServiceDependency(serviceName, depend, version) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | inv.logger.Infof("Added service dependency! service=%s, dep=%s, port=%d, version=%s", serviceName, dependServiceName, egressPort, version) 152 | return nil 153 | } 154 | 155 | // RemoveServiceDependencies removes a service dependency from the service. 156 | func (inv *inventoryService) RemoveServiceDependency(serviceName string, dependServiceName string) (bool, error) { 157 | version := inv.versionGen.New() 158 | ok, err := inv.repo.RemoveServiceDependency(serviceName, dependServiceName, version) 159 | if ok { 160 | inv.logger.Infof("Removed service dependency! service=%s, dep=%s, version=%s", serviceName, dependServiceName, version) 161 | } 162 | return ok, err 163 | } 164 | 165 | // RegisterHost stores a new Host object. 166 | // TODO: with consistency 167 | func (inv *inventoryService) RegisterHost(serviceName, hostName, ingressAddr, substanceAddr, egressHost string) (host model.Host, err error) { 168 | host, err = model.NewHost(hostName, ingressAddr, substanceAddr, egressHost) 169 | if err != nil { 170 | return host, err 171 | } 172 | 173 | err = host.Validate() 174 | if err != nil { 175 | return host, err 176 | } 177 | 178 | // check dup 179 | names, err := inv.repo.SelectAllHostNames() 180 | if err != nil { 181 | return host, err 182 | } 183 | _, ok := utils.ContainsString(names, hostName) 184 | if ok { 185 | return host, fmt.Errorf("host %s already exists", hostName) 186 | } 187 | 188 | // get the service 189 | service, ok, err := inv.GetService(serviceName) 190 | if err != nil { 191 | return host, err 192 | } 193 | if !ok { 194 | return host, fmt.Errorf("No such service: %s", serviceName) 195 | } 196 | 197 | // save the host 198 | err = inv.repo.PutHost(host) 199 | if err != nil { 200 | return host, err 201 | } 202 | inv.logger.Infof("Host registered! host=%s, service=%s", hostName, serviceName) 203 | 204 | // update the service list 205 | version := inv.versionGen.New() 206 | service.HostNames = append(service.HostNames, hostName) 207 | err = inv.repo.PutService(service, version) 208 | if err != nil { 209 | return host, errors.Wrapf(err, "failed to append a host(%s) to the service(%s)", hostName, serviceName) 210 | } 211 | inv.logger.Infof("Added a host to service's host list! host=%s, service=%s, version=%s", hostName, serviceName, version) 212 | 213 | // register the host to discovery service if discovery is available 214 | err = inv.registerDiscoverService(&service, &host) 215 | if err != nil { 216 | return host, err 217 | } 218 | 219 | return host, nil 220 | } 221 | 222 | // UnregisterHost removes the Host object and hostname in host list of a service 223 | func (inv *inventoryService) UnregisterHost(serviceName string, hostName string) (bool, error) { 224 | svc, ok, err := inv.GetService(serviceName) 225 | if err != nil { 226 | return false, err 227 | } 228 | if !ok { 229 | return false, nil 230 | } 231 | 232 | i, ok := utils.ContainsString(svc.HostNames, hostName) 233 | if !ok { 234 | return false, nil 235 | } 236 | // remove from service's host list and update service version 237 | svc.HostNames = append(svc.HostNames[:i], svc.HostNames[i+1:]...) 238 | version := inv.versionGen.New() 239 | err = inv.repo.PutService(svc, version) 240 | if err != nil { 241 | return false, err 242 | } 243 | inv.logger.Infof("Removed a host from service's host list! host=%s, service=%s, version=%s", hostName, serviceName, version) 244 | 245 | // unregister the host from discovery service if discovery is available 246 | err = inv.unregisterDiscoverService(hostName) 247 | if err != nil { 248 | return false, err 249 | } 250 | 251 | ok, err = inv.repo.DeleteHost(hostName) 252 | if ok { 253 | inv.logger.Infof("Host is removed! host=%s, service=%s", hostName, serviceName) 254 | } 255 | return ok, err 256 | } 257 | 258 | // GetHost finds a host by name. 259 | func (inv *inventoryService) GetHostByName(name string) (model.Host, bool, error) { 260 | return inv.repo.SelectHostByName(name) 261 | } 262 | 263 | // GetHostNames returns the names of all hosts 264 | func (inv *inventoryService) GetHostNames() ([]string, error) { 265 | return inv.repo.SelectAllHostNames() 266 | } 267 | 268 | // GetHostOfService returns the objects of all hosts to which the service belongs 269 | func (inv *inventoryService) GetHostsOfService(serviceName string) ([]model.Host, error) { 270 | return inv.repo.SelectHostsOfService(serviceName) 271 | } 272 | 273 | // UpdateHost updates a host. 274 | func (inv *inventoryService) UpdateHost(serviceName string, hostName string, ingressAddr, substanceAddr, egressHost *string) (host model.Host, err error) { 275 | svc, ok, err := inv.GetService(serviceName) 276 | if err != nil { 277 | return host, err 278 | } 279 | _, ok = utils.ContainsString(svc.HostNames, hostName) 280 | if !ok { 281 | return host, fmt.Errorf("hostname=%s is not found in service=%s", hostName, serviceName) 282 | } 283 | 284 | host, ok, err = inv.GetHostByName(hostName) 285 | if err != nil { 286 | return host, err 287 | } 288 | if !ok { 289 | return host, fmt.Errorf("hostname=%s is not found in service=%s", hostName, serviceName) 290 | } 291 | 292 | err = host.Update(ingressAddr, substanceAddr, egressHost) 293 | if err != nil { 294 | return host, err 295 | } 296 | 297 | err = host.Validate() 298 | if err != nil { 299 | return host, err 300 | } 301 | 302 | // save the host 303 | version := inv.versionGen.New() 304 | err = inv.repo.PutHost(host) 305 | if err != nil { 306 | return host, err 307 | } 308 | inv.logger.Infof("Updated a host! host=%s, service=%s, ia=%v, sa=%v, eh=%v", hostName, serviceName, ingressAddr, substanceAddr, egressHost) 309 | 310 | // update the service version 311 | err = inv.repo.PutService(svc, version) 312 | if err != nil { 313 | return host, errors.Wrapf(err, "failed to update service version(%s)", svc.Name) 314 | } 315 | inv.logger.Infof("Updated a service! host=%s, service=%s, version=%s", hostName, serviceName, version) 316 | 317 | return host, nil 318 | } 319 | 320 | // IdempotentService updates service and its hosts idempotently. 321 | func (inv *inventoryService) IdempotentService(serviceName string, param model.IdempotentServiceParam) (changed bool, err error) { 322 | var i int 323 | // validate 324 | service := param.NewService(serviceName) 325 | err = service.Validate() 326 | if err != nil { 327 | return changed, err 328 | } 329 | paramHostsMap := map[string]*model.Host{} 330 | for i = 0; i < len(param.Hosts); i++ { 331 | err = param.Hosts[i].Validate() 332 | if err != nil { 333 | return changed, err 334 | } 335 | paramHostsMap[param.Hosts[i].Name] = ¶m.Hosts[i] 336 | } 337 | 338 | // get the current service state 339 | currentService, ok, err := inv.GetService(serviceName) 340 | if err != nil { 341 | return changed, err 342 | } 343 | 344 | if ok { 345 | // update 346 | // get current host states 347 | hosts, err := inv.GetHostsOfService(serviceName) 348 | if err != nil { 349 | return changed, err 350 | } 351 | currentHostsMap := map[string]*model.Host{} 352 | currentHostNames := make([]string, len(hosts)) 353 | for i = 0; i < len(hosts); i++ { 354 | currentHostNames[i] = hosts[i].Name 355 | currentHostsMap[hosts[i].Name] = &hosts[i] 356 | } 357 | 358 | // compare hostnames 359 | newHosts := utils.FilterNotContainsString(service.HostNames, currentHostNames) 360 | delHosts := utils.FilterNotContainsString(currentHostNames, service.HostNames) 361 | modifiedHosts := utils.IntersectStringSlice(currentHostNames, service.HostNames) 362 | // register appended hosts 363 | for _, hostname := range newHosts { 364 | host, ok := paramHostsMap[hostname] 365 | if !ok { 366 | return changed, fmt.Errorf("something wrong, consistency may be broken: %+v, %+v", paramHostsMap, hosts) 367 | } 368 | _, err = inv.RegisterHost(service.Name, host.Name, host.IngressAddr.String(), host.SubstanceAddr.String(), host.EgressHost) 369 | if err != nil { 370 | return changed, err 371 | } 372 | changed = true 373 | } 374 | // unregister disappeared hosts 375 | for _, hostname := range delHosts { 376 | _, err = inv.UnregisterHost(service.Name, hostname) 377 | if err != nil { 378 | return changed, err 379 | } 380 | changed = true 381 | } 382 | // update the hosts that is modified 383 | for _, hostname := range modifiedHosts { 384 | cur, ok1 := currentHostsMap[hostname] 385 | new, ok2 := paramHostsMap[hostname] 386 | if !ok1 || !ok2 { 387 | return changed, fmt.Errorf("something wrong, consistency may be broken: %+v : %+v", paramHostsMap, hosts) 388 | } 389 | if !reflect.DeepEqual(cur, new) { 390 | ingress := new.IngressAddr.String() 391 | substance := new.SubstanceAddr.String() 392 | _, err = inv.UpdateHost(serviceName, new.Name, &ingress, &substance, &new.EgressHost) 393 | if err != nil { 394 | return changed, err 395 | } 396 | changed = true 397 | } 398 | } 399 | 400 | // compare service dependencies and protocol 401 | if (service.Protocol != currentService.Protocol) || 402 | (!model.EqualsServiceDependencies(currentService.DependentServices, service.DependentServices)) { 403 | service.Version = inv.versionGen.New() 404 | err := inv.repo.PutService(service, service.Version) 405 | if err != nil { 406 | return changed, err 407 | } 408 | changed = true 409 | } 410 | 411 | } else { 412 | // all new ones 413 | _, err = inv.RegisterService(service.Name, service.Protocol) 414 | if err != nil { 415 | return changed, err 416 | } 417 | changed = true 418 | for _, depsvc := range param.DependentServices { 419 | err = inv.AddServiceDependency(service.Name, depsvc.Name, depsvc.EgressPort) 420 | if err != nil { 421 | return changed, err 422 | } 423 | } 424 | for i = 0; i < len(param.Hosts); i++ { 425 | host := ¶m.Hosts[i] 426 | _, err = inv.RegisterHost(service.Name, host.Name, host.IngressAddr.String(), host.SubstanceAddr.String(), host.EgressHost) 427 | if err != nil { 428 | return changed, err 429 | } 430 | } 431 | } 432 | if changed { 433 | inv.logger.Infof("Updated service via idempotent function! service=%s", serviceName) 434 | } 435 | return changed, nil 436 | } 437 | 438 | func (inv *inventoryService) registerDiscoverService(svc *model.Service, host *model.Host) error { 439 | if inv.discovery != nil { 440 | tags := inv.makeDiscoveryTags(svc, host) 441 | err := inv.discovery.Register(*host, tags) 442 | if err != nil { 443 | return err 444 | } 445 | inv.logger.Infof("Registered a host to discovery service! host=%s, service=%s", host.Name, svc.Name) 446 | } 447 | return nil 448 | } 449 | 450 | func (inv *inventoryService) makeDiscoveryTags(service *model.Service, host *model.Host) map[string]string { 451 | tags := map[string]string{} 452 | tags["meshem_service"] = service.Name 453 | return tags 454 | } 455 | 456 | func (inv *inventoryService) unregisterDiscoverService(hostname string) error { 457 | if inv.discovery != nil { 458 | err := inv.discovery.Unregister(hostname) 459 | if err != nil { 460 | return err 461 | } 462 | inv.logger.Infof("Remove a host from discovery service! host=%s", hostname) 463 | } 464 | return nil 465 | } 466 | -------------------------------------------------------------------------------- /src/core/inventory_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/rerorero/meshem/src/model" 7 | "github.com/rerorero/meshem/src/repository" 8 | "github.com/sirupsen/logrus" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | type MockedDiscoveryRepository struct { 14 | mock.Mock 15 | } 16 | 17 | func (mdr *MockedDiscoveryRepository) Register(host model.Host, tags map[string]string) error { 18 | args := mdr.Called(host, tags) 19 | return args.Error(0) 20 | } 21 | func (mdr *MockedDiscoveryRepository) Unregister(hostname string) error { 22 | args := mdr.Called(hostname) 23 | return args.Error(0) 24 | } 25 | func (mdr *MockedDiscoveryRepository) FindByName(hostname string) (*repository.DiscoveryInfo, bool, error) { 26 | args := mdr.Called(hostname) 27 | return args.Get(0).(*repository.DiscoveryInfo), args.Bool(1), args.Error(2) 28 | } 29 | 30 | func TestRegisterService(t *testing.T) { 31 | repo := repository.NewInventoryHeap() 32 | discovery := MockedDiscoveryRepository{} 33 | gen := &MockedVersionGen{Version: "abc"} 34 | sut := NewInventoryService(repo, &discovery, gen, logrus.New()) 35 | 36 | svc, err := sut.RegisterService("svc1", model.ProtocolHTTP) 37 | assert.NoError(t, err) 38 | 39 | actualsvc, ok, err := sut.GetService("svc1") 40 | assert.NoError(t, err) 41 | assert.True(t, ok) 42 | assert.Equal(t, actualsvc, model.Service{Name: "svc1", Protocol: model.ProtocolHTTP, Version: gen.Version, TraceSpan: "svc1"}) 43 | actual, err := repo.SelectAllServiceNames() 44 | assert.NoError(t, err) 45 | assert.ElementsMatch(t, actual, []string{svc.Name}) 46 | 47 | // duplicates 48 | _, err = sut.RegisterService("svc1", model.ProtocolTCP) 49 | assert.Error(t, err) 50 | // validation failed 51 | _, err = sut.RegisterService(" in va lid", model.ProtocolHTTP) 52 | assert.Error(t, err) 53 | 54 | svc2, err := sut.RegisterService("svc2", model.ProtocolHTTP) 55 | assert.NoError(t, err) 56 | 57 | svcs, err := repo.SelectAllServices() 58 | assert.NoError(t, err) 59 | assert.ElementsMatch(t, svcs, []model.Service{svc, svc2}) 60 | 61 | ok, _, err = sut.UnregisterService("svc3") 62 | assert.NoError(t, err) 63 | assert.False(t, ok) 64 | actual, err = repo.SelectAllServiceNames() 65 | assert.NoError(t, err) 66 | assert.ElementsMatch(t, actual, []string{svc.Name, svc2.Name}) 67 | 68 | ok, _, err = sut.UnregisterService("svc1") 69 | assert.NoError(t, err) 70 | assert.True(t, ok) 71 | actual, err = repo.SelectAllServiceNames() 72 | assert.NoError(t, err) 73 | assert.ElementsMatch(t, actual, []string{svc2.Name}) 74 | } 75 | 76 | func TestRegisterHost(t *testing.T) { 77 | repo := repository.NewInventoryHeap() 78 | discovery := MockedDiscoveryRepository{} 79 | gen := &MockedVersionGen{Version: "abc"} 80 | sut := NewInventoryService(repo, &discovery, gen, logrus.New()) 81 | 82 | svc, err := sut.RegisterService("svc1", model.ProtocolHTTP) 83 | assert.NoError(t, err) 84 | gen.Version = "def" 85 | 86 | expect, err := model.NewHost("host1", "192.168.0.1:8081", "127.0.0.1:8080", "127.0.0.1") 87 | expectTags := map[string]string{} 88 | expectTags["meshem_service"] = "svc1" 89 | assert.NoError(t, err) 90 | discovery.On("Register", expect, expectTags).Return(nil) 91 | host1, err := sut.RegisterHost(svc.Name, "host1", "192.168.0.1:8081", "127.0.0.1:8080", "127.0.0.1") 92 | assert.NoError(t, err) 93 | assert.Equal(t, expect, host1) 94 | 95 | actualHost, ok, err := sut.GetHostByName("host1") 96 | assert.NoError(t, err) 97 | assert.True(t, ok) 98 | assert.Equal(t, actualHost, host1) 99 | // registered host in service 100 | actualSvc, ok, err := sut.GetService(svc.Name) 101 | assert.NoError(t, err) 102 | assert.True(t, ok) 103 | // svc = expected 104 | svc.Version = "def" 105 | svc.HostNames = []string{"host1"} 106 | assert.Equal(t, svc, actualSvc) 107 | 108 | // unregister 109 | discovery.On("Unregister", "host1").Return(nil) 110 | deleted, err := sut.UnregisterHost("svc1", "host1") 111 | assert.NoError(t, err) 112 | assert.True(t, deleted) 113 | } 114 | 115 | func TestServiceDependencies(t *testing.T) { 116 | repo := repository.NewInventoryHeap() 117 | discovery := MockedDiscoveryRepository{} 118 | gen := &MockedVersionGen{Version: "abc"} 119 | sut := NewInventoryService(repo, &discovery, gen, logrus.New()) 120 | 121 | svcC := &model.Service{ 122 | Name: "serviceC", 123 | Version: "abc", 124 | Protocol: model.ProtocolHTTP, 125 | } 126 | 127 | svcB := &model.Service{ 128 | Name: "serviceB", 129 | Version: "def", 130 | Protocol: model.ProtocolTCP, 131 | } 132 | 133 | svcA := &model.Service{ 134 | Name: "serviceA", 135 | Version: "ghi", 136 | Protocol: model.ProtocolTCP, 137 | } 138 | allsvc := []*model.Service{svcA, svcB, svcC} 139 | 140 | depB := []model.DependentService{ 141 | model.DependentService{ 142 | Name: svcC.Name, 143 | EgressPort: 9001, 144 | }, 145 | } 146 | depA := []model.DependentService{ 147 | model.DependentService{ 148 | Name: svcC.Name, 149 | EgressPort: 9001, 150 | }, 151 | model.DependentService{ 152 | Name: svcB.Name, 153 | EgressPort: 9002, 154 | }, 155 | } 156 | 157 | var err error 158 | // register services without dependencies 159 | for _, svc := range allsvc { 160 | _, err := sut.RegisterService(svc.Name, svc.Protocol) 161 | assert.NoError(t, err) 162 | } 163 | names, err := sut.GetServiceNames() 164 | assert.NoError(t, err) 165 | assert.ElementsMatch(t, []string{svcA.Name, svcB.Name, svcC.Name}, names) 166 | 167 | // add dependencies 168 | gen.Version = "newnew" 169 | for _, dep := range depA { 170 | err = sut.AddServiceDependency(svcA.Name, dep.Name, dep.EgressPort) 171 | assert.NoError(t, err) 172 | } 173 | for _, dep := range depB { 174 | err = sut.AddServiceDependency(svcB.Name, dep.Name, dep.EgressPort) 175 | assert.NoError(t, err) 176 | } 177 | actualsvc, ok, err := sut.GetService(svcA.Name) 178 | assert.NoError(t, err) 179 | assert.True(t, ok) 180 | assert.Equal(t, actualsvc.Version, model.Version("newnew")) 181 | assert.ElementsMatch(t, actualsvc.DependentServices, depA) 182 | 183 | // remove service 184 | refs, err := sut.GetRefferersOf(svcC.Name) 185 | assert.NoError(t, err) 186 | assert.ElementsMatch(t, refs, []string{svcA.Name, svcB.Name}) 187 | 188 | ok, refs, err = sut.UnregisterService(svcB.Name) 189 | assert.NoError(t, err) 190 | assert.True(t, ok) 191 | assert.ElementsMatch(t, refs, []string{svcA.Name}) 192 | 193 | refs, err = sut.GetRefferersOf(svcC.Name) 194 | assert.NoError(t, err) 195 | assert.ElementsMatch(t, refs, []string{svcA.Name}) 196 | } 197 | 198 | func TestIdemopotentService(t *testing.T) { 199 | repo := repository.NewInventoryHeap() 200 | gen := &MockedVersionGen{Version: "abc"} 201 | sut := NewInventoryService(repo, nil, gen, logrus.New()) 202 | 203 | svcA := model.IdempotentServiceParam{ 204 | Protocol: "HTTP", 205 | Hosts: []model.Host{ 206 | { 207 | Name: "a-1", 208 | IngressAddr: model.Address{Hostname: "192.168.0.1", Port: 8000}, 209 | SubstanceAddr: model.Address{Hostname: "127.0.0.1", Port: 8001}, 210 | EgressHost: "127.0.0.1", 211 | }, 212 | { 213 | Name: "a-2", 214 | IngressAddr: model.Address{Hostname: "192.168.0.2", Port: 8000}, 215 | SubstanceAddr: model.Address{Hostname: "127.0.0.1", Port: 8001}, 216 | EgressHost: "127.0.0.1", 217 | }, 218 | }, 219 | } 220 | svcB := model.IdempotentServiceParam{ 221 | Protocol: "TCP", 222 | DependentServices: []model.DependentService{ 223 | { 224 | Name: "svcA", 225 | EgressPort: 9002, 226 | }, 227 | }, 228 | Hosts: []model.Host{ 229 | { 230 | Name: "b-1", 231 | IngressAddr: model.Address{Hostname: "192.168.0.2", Port: 9000}, 232 | SubstanceAddr: model.Address{Hostname: "127.0.0.1", Port: 9001}, 233 | EgressHost: "127.0.0.1", 234 | }, 235 | }, 236 | } 237 | 238 | // put A 239 | changed, err := sut.IdempotentService("svcA", svcA) 240 | assert.NoError(t, err) 241 | assert.True(t, changed) 242 | changed, err = sut.IdempotentService("svcA", svcA) 243 | assert.NoError(t, err) 244 | assert.False(t, changed) // not changed when applied twice 245 | 246 | actualsvc, ok, err := sut.GetService("svcA") 247 | assert.NoError(t, err) 248 | assert.True(t, ok) 249 | assert.Equal(t, model.Service{ 250 | Name: "svcA", 251 | HostNames: []string{"a-1", "a-2"}, 252 | DependentServices: nil, 253 | Protocol: svcA.Protocol, 254 | Version: "abc", 255 | TraceSpan: "svcA", 256 | }, actualsvc) 257 | actualHosts, err := sut.GetHostsOfService("svcA") 258 | assert.ElementsMatch(t, svcA.Hosts, actualHosts) 259 | 260 | // put B 261 | gen.Version = "bbb" 262 | changed, err = sut.IdempotentService("svcB", svcB) 263 | assert.NoError(t, err) 264 | assert.True(t, changed) 265 | 266 | actualsvc, ok, err = sut.GetService("svcB") 267 | assert.NoError(t, err) 268 | assert.True(t, ok) 269 | assert.Equal(t, model.Service{ 270 | Name: "svcB", 271 | HostNames: []string{"b-1"}, 272 | DependentServices: svcB.DependentServices, 273 | Protocol: svcB.Protocol, 274 | Version: "bbb", 275 | TraceSpan: "svcB", 276 | }, actualsvc) 277 | actualHosts, err = sut.GetHostsOfService("svcB") 278 | assert.ElementsMatch(t, svcB.Hosts, actualHosts) 279 | // svcA's version should not be updated 280 | actualsvc, ok, err = sut.GetService("svcA") 281 | assert.NoError(t, err) 282 | assert.Equal(t, model.Version("abc"), actualsvc.Version) 283 | 284 | // update svcA 285 | gen.Version = "ccc" 286 | svcAMod := model.IdempotentServiceParam{ 287 | Protocol: "TCP", 288 | Hosts: []model.Host{ 289 | { 290 | Name: "a-2", 291 | IngressAddr: model.Address{Hostname: "192.168.99.2", Port: 9000}, 292 | SubstanceAddr: model.Address{Hostname: "localhost", Port: 9001}, 293 | EgressHost: "localhost", 294 | }, 295 | { 296 | Name: "a-3", 297 | IngressAddr: model.Address{Hostname: "192.168.0.3", Port: 9000}, 298 | SubstanceAddr: model.Address{Hostname: "127.0.0.1", Port: 9001}, 299 | EgressHost: "127.0.0.1", 300 | }, 301 | }, 302 | } 303 | changed, err = sut.IdempotentService("svcA", svcAMod) 304 | assert.NoError(t, err) 305 | assert.True(t, changed) 306 | 307 | actualsvc, ok, err = sut.GetService("svcA") 308 | assert.NoError(t, err) 309 | assert.True(t, ok) 310 | assert.Equal(t, model.Service{ 311 | Name: "svcA", 312 | HostNames: []string{"a-2", "a-3"}, 313 | DependentServices: nil, 314 | Protocol: svcAMod.Protocol, 315 | Version: "ccc", 316 | TraceSpan: "svcA", 317 | }, actualsvc) 318 | actualHosts, err = sut.GetHostsOfService("svcA") 319 | assert.ElementsMatch(t, svcAMod.Hosts, actualHosts) 320 | 321 | // update svcB 322 | gen.Version = "ddd" 323 | svcC := model.IdempotentServiceParam{Protocol: "HTTP"} 324 | changed, err = sut.IdempotentService("svcC", svcC) 325 | assert.NoError(t, err) 326 | assert.True(t, changed) 327 | svcBMod := model.IdempotentServiceParam{ 328 | Protocol: "TCP", 329 | DependentServices: []model.DependentService{ 330 | { 331 | Name: "svcA", 332 | EgressPort: 9003, 333 | }, 334 | { 335 | Name: "svcC", 336 | EgressPort: 9004, 337 | }, 338 | }, 339 | Hosts: []model.Host{ 340 | { 341 | Name: "b-1", 342 | IngressAddr: model.Address{Hostname: "192.168.0.2", Port: 9000}, 343 | SubstanceAddr: model.Address{Hostname: "127.0.0.1", Port: 9001}, 344 | EgressHost: "127.0.0.1", 345 | }, 346 | }, 347 | } 348 | gen.Version = "eee" 349 | changed, err = sut.IdempotentService("svcB", svcBMod) 350 | assert.NoError(t, err) 351 | assert.True(t, changed) 352 | 353 | actualsvc, ok, err = sut.GetService("svcB") 354 | assert.NoError(t, err) 355 | assert.True(t, ok) 356 | assert.Equal(t, model.Service{ 357 | Name: "svcB", 358 | HostNames: []string{"b-1"}, 359 | DependentServices: svcBMod.DependentServices, 360 | Protocol: svcBMod.Protocol, 361 | Version: "eee", 362 | TraceSpan: "svcB", 363 | }, actualsvc) 364 | actualHosts, err = sut.GetHostsOfService("svcB") 365 | assert.ElementsMatch(t, svcBMod.Hosts, actualHosts) 366 | } 367 | -------------------------------------------------------------------------------- /src/core/version_gen.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/rerorero/meshem/src/model" 8 | ) 9 | 10 | type VersionGenerator interface { 11 | New() model.Version 12 | Compare(l, r model.Version) int 13 | } 14 | 15 | type currentTimeGen struct{} 16 | 17 | func NewCurrentTimeGenerator() VersionGenerator { 18 | return ¤tTimeGen{} 19 | } 20 | 21 | // TODO: generate a distributed sequential unique number (in datastore?) 22 | func (gen *currentTimeGen) New() model.Version { 23 | now := time.Now().UnixNano() / int64(time.Millisecond) 24 | return model.Version(strconv.FormatInt(now, 10)) 25 | } 26 | 27 | // Compare returns 0 if l==r, -1 if l>r, +1 if l rn { 43 | return -1 44 | } else if ln < rn { 45 | return 1 46 | } 47 | return 0 48 | } 49 | 50 | // MockedVersionGen is mock generator for testing 51 | type MockedVersionGen struct { 52 | Version model.Version 53 | CompareResult int 54 | } 55 | 56 | func (gen *MockedVersionGen) New() model.Version { 57 | return gen.Version 58 | } 59 | 60 | func (gen *MockedVersionGen) Compare(l, r model.Version) int { 61 | return gen.CompareResult 62 | } 63 | -------------------------------------------------------------------------------- /src/core/xds/hasher.go: -------------------------------------------------------------------------------- 1 | package xds 2 | 3 | import "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 4 | 5 | // Hasher returns node ID as an ID 6 | type Hasher struct { 7 | } 8 | 9 | // ID function 10 | func (h Hasher) ID(node *core.Node) string { 11 | if node == nil { 12 | return "unknown" 13 | } 14 | return node.Id 15 | } 16 | -------------------------------------------------------------------------------- /src/core/xds/healthcheck.go: -------------------------------------------------------------------------------- 1 | package xds 2 | 3 | import ( 4 | "time" 5 | 6 | hc "github.com/envoyproxy/go-control-plane/envoy/config/filter/http/health_check/v2" 7 | hcm "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2" 8 | "github.com/envoyproxy/go-control-plane/pkg/util" 9 | "github.com/gogo/protobuf/types" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // HTTPHealthCheck TODO: to be able to configure via ctlapi 14 | type HTTPHealthCheck struct { 15 | Enabled bool 16 | PassThrough bool 17 | Endpoint string 18 | CacheTime time.Duration 19 | } 20 | 21 | // NewDisabledHTTPHealthCheck creates default health check configuration. 22 | func NewDisabledHTTPHealthCheck() *HTTPHealthCheck { 23 | return &HTTPHealthCheck{ 24 | Enabled: true, 25 | } 26 | } 27 | 28 | // NewDefaultPassThroghHTTPHealthCheck creates default health check configuration. 29 | func NewDefaultPassThroghHTTPHealthCheck() *HTTPHealthCheck { 30 | return &HTTPHealthCheck{ 31 | Enabled: true, 32 | PassThrough: true, 33 | Endpoint: "/", 34 | CacheTime: 5 * time.Second, 35 | } 36 | } 37 | 38 | func (hhc *HTTPHealthCheck) createEnvoyHTTPFilter() (*hcm.HttpFilter, error) { 39 | if !hhc.Enabled { 40 | return nil, nil 41 | } 42 | config := &hc.HealthCheck{ 43 | PassThroughMode: &types.BoolValue{Value: hhc.PassThrough}, 44 | Endpoint: hhc.Endpoint, 45 | } 46 | if hhc.PassThrough { 47 | config.CacheTime = &hhc.CacheTime 48 | } 49 | pbst, err := util.MessageToStruct(config) 50 | if err != nil { 51 | return nil, errors.Wrapf(err, "failed to create health check %+v", hhc) 52 | } 53 | return &hcm.HttpFilter{ 54 | Name: "envoy.health_check", 55 | Config: pbst, 56 | }, nil 57 | } 58 | -------------------------------------------------------------------------------- /src/core/xds/server.go: -------------------------------------------------------------------------------- 1 | package xds 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 10 | discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2" 11 | "github.com/envoyproxy/go-control-plane/pkg/cache" 12 | xds "github.com/envoyproxy/go-control-plane/pkg/server" 13 | "github.com/pkg/errors" 14 | mcore "github.com/rerorero/meshem/src/core" 15 | "github.com/rerorero/meshem/src/model" 16 | "github.com/sirupsen/logrus" 17 | "google.golang.org/grpc" 18 | ) 19 | 20 | type XDSServer interface { 21 | RunXDS() (*grpc.Server, error) 22 | RunSnapshotCollector() 23 | } 24 | 25 | type xdss struct { 26 | inventory mcore.InventoryService 27 | snapshotCache cache.SnapshotCache 28 | snapshotGen SnapshotGen 29 | conf model.XDSConf 30 | ctx context.Context 31 | logger *logrus.Logger 32 | } 33 | 34 | // NewXDSServer creates a xds server. 35 | func NewXDSServer(inventory mcore.InventoryService, vb mcore.VersionGenerator, conf model.MeshemConf, ctx context.Context, logger *logrus.Logger) XDSServer { 36 | return &xdss{ 37 | inventory: inventory, 38 | snapshotCache: cache.NewSnapshotCache(conf.XDS.IsADSMode, Hasher{}, &snapshotLogger{logger}), 39 | snapshotGen: NewSnapshotGen(inventory, logger, vb, conf.Envoy), 40 | conf: conf.XDS, 41 | ctx: ctx, 42 | logger: logger, 43 | } 44 | } 45 | 46 | type snapshotLogger struct { 47 | logger *logrus.Logger 48 | } 49 | 50 | // Infof logs a formatted informational message. 51 | func (l *snapshotLogger) Infof(format string, args ...interface{}) { 52 | l.logger.Infof(format, args...) 53 | } 54 | 55 | // Errorf logs a formatted error message. 56 | func (l *snapshotLogger) Errorf(format string, args ...interface{}) { 57 | l.logger.Errorf(format, args...) 58 | } 59 | 60 | func (s *xdss) RunXDS() (*grpc.Server, error) { 61 | grpcServer := grpc.NewServer() 62 | server := xds.NewServer(s.snapshotCache, nil) 63 | lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.conf.Port)) 64 | if err != nil { 65 | return nil, err 66 | } 67 | discovery.RegisterAggregatedDiscoveryServiceServer(grpcServer, server) 68 | v2.RegisterEndpointDiscoveryServiceServer(grpcServer, server) 69 | v2.RegisterClusterDiscoveryServiceServer(grpcServer, server) 70 | v2.RegisterRouteDiscoveryServiceServer(grpcServer, server) 71 | v2.RegisterListenerDiscoveryServiceServer(grpcServer, server) 72 | s.logger.Infof("xDS server listening on %d", s.conf.Port) 73 | 74 | go func() { 75 | if err = grpcServer.Serve(lis); err != nil { 76 | s.logger.Error(err) 77 | } 78 | }() 79 | 80 | return grpcServer, nil 81 | } 82 | 83 | func (s *xdss) RunSnapshotCollector() { 84 | ticker := time.NewTicker(time.Duration(s.conf.CacheCollectionIntervalMS) * time.Millisecond) 85 | go func() { 86 | s.logger.Info("snapshot collector started.") 87 | for { 88 | select { 89 | case <-ticker.C: 90 | s.saveSnapshots() 91 | case <-s.ctx.Done(): 92 | ticker.Stop() 93 | s.logger.Info("snapshot collector finished.") 94 | return 95 | } 96 | } 97 | }() 98 | } 99 | 100 | func (s *xdss) saveSnapshots() error { 101 | // TODO: Copy all data from the datastore to the heap(repository) to reduce the access to the datastore (and to read consistently when we use an ACID datastore). 102 | 103 | allsvc, err := s.inventory.GetServiceNames() 104 | if err != nil { 105 | return err 106 | } 107 | 108 | for _, svc := range allsvc { 109 | snapshots, err := s.snapshotGen.MakeSnapshotsOfService(svc) 110 | if err != nil { 111 | s.logger.Errorf("failed to generate snapshot: %s", svc) 112 | s.logger.Error(err) 113 | continue 114 | } 115 | for host, snapshot := range snapshots { 116 | s.logger.Infof("set snapshot %s: %+v", host.Name, *snapshot) 117 | err = s.snapshotCache.SetSnapshot(host.Name, *snapshot) 118 | if err != nil { 119 | return errors.Wrapf(err, "snapshot failed: %s=%+v of", host.Name, snapshot) 120 | } 121 | } 122 | } 123 | 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /src/core/xds/snapshot_gen.go: -------------------------------------------------------------------------------- 1 | package xds 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 8 | "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 9 | "github.com/envoyproxy/go-control-plane/envoy/api/v2/endpoint" 10 | "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener" 11 | "github.com/envoyproxy/go-control-plane/envoy/api/v2/route" 12 | accesslog "github.com/envoyproxy/go-control-plane/envoy/config/filter/accesslog/v2" 13 | hcm "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2" 14 | tcp "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/tcp_proxy/v2" 15 | "github.com/envoyproxy/go-control-plane/pkg/cache" 16 | "github.com/envoyproxy/go-control-plane/pkg/util" 17 | "github.com/pkg/errors" 18 | mcore "github.com/rerorero/meshem/src/core" 19 | "github.com/rerorero/meshem/src/model" 20 | "github.com/sirupsen/logrus" 21 | ) 22 | 23 | type SnapshotGen interface { 24 | MakeSnapshotsOfService(serviceName string) (snapshots map[*model.Host]*cache.Snapshot, err error) 25 | } 26 | 27 | type snapGen struct { 28 | inventory mcore.InventoryService 29 | logger *logrus.Logger 30 | versionGen mcore.VersionGenerator 31 | envoyConf model.EnvoyConf 32 | } 33 | 34 | const ( 35 | // XdsCluster is the cluster name for the control server (used by non-ADS set-up) 36 | XdsCluster = "xds_cluster" 37 | ) 38 | 39 | // NewSnapshotGen creates snapshot generator instance. 40 | func NewSnapshotGen(is mcore.InventoryService, logger *logrus.Logger, vg mcore.VersionGenerator, envoyConf model.EnvoyConf) SnapshotGen { 41 | return &snapGen{ 42 | inventory: is, 43 | logger: logger, 44 | versionGen: vg, 45 | envoyConf: envoyConf, 46 | } 47 | } 48 | 49 | // FindSnapshotByName finds a snapshot by hostname from a snapshot map 50 | func FindSnapshotByName(snapshots map[*model.Host]*cache.Snapshot, hostname string) (*cache.Snapshot, bool) { 51 | for h, s := range snapshots { 52 | if h.Name == hostname { 53 | return s, true 54 | } 55 | } 56 | return nil, false 57 | } 58 | 59 | func (gen *snapGen) MakeSnapshotsOfService(serviceName string) (map[*model.Host]*cache.Snapshot, error) { 60 | // get service 61 | service, ok, err := gen.inventory.GetService(serviceName) 62 | if err != nil { 63 | return nil, err 64 | } 65 | if !ok { 66 | return nil, fmt.Errorf("service=%s not found", serviceName) 67 | } 68 | 69 | // get service dependencies 70 | dependencies := map[*model.Service][]model.Host{} 71 | var i int 72 | for i = 0; i < len(service.DependentServices); i++ { 73 | name := service.DependentServices[i].Name 74 | depHosts, err := gen.inventory.GetHostsOfService(name) 75 | if err != nil { 76 | return nil, err 77 | } 78 | depService, ok, err := gen.inventory.GetService(name) 79 | if err != nil { 80 | return nil, err 81 | } 82 | if !ok { 83 | return nil, fmt.Errorf("depndencies of %s not found", name) 84 | } 85 | dependencies[&depService] = depHosts 86 | } 87 | 88 | hosts, err := gen.inventory.GetHostsOfService(service.Name) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | snapshots := map[*model.Host]*cache.Snapshot{} 94 | for i = 0; i < len(hosts); i++ { 95 | snapshot, err := gen.makeHostSnapshot(&service, &hosts[i], dependencies) 96 | if err != nil { 97 | return nil, errors.Wrapf(err, "udpate snapshot failed: service=%+v, host=%+v", service, hosts[i]) 98 | } 99 | err = snapshot.Consistent() 100 | if err != nil { 101 | return nil, errors.Wrapf(err, "snapshot incosistency: %+v", snapshot) 102 | } 103 | snapshots[&hosts[i]] = snapshot 104 | } 105 | return snapshots, nil 106 | } 107 | 108 | func (gen *snapGen) makeHostSnapshot(service *model.Service, host *model.Host, dependencies map[*model.Service][]model.Host) (*cache.Snapshot, error) { 109 | clusters := []cache.Resource{} 110 | endpoints := []cache.Resource{} 111 | routes := []cache.Resource{} 112 | listeners := []cache.Resource{} 113 | defaultTimeout := time.Duration(gen.envoyConf.ClusterTimeoutMS) * time.Millisecond 114 | 115 | // version of the data to be cached 116 | version := gen.latestNodeVersion(service, dependencies) 117 | 118 | // ingress 119 | ingressClusterName := "ingress" 120 | clusters = append(clusters, MakeEDSCluster(ingressClusterName, defaultTimeout)) 121 | endpoints = append(endpoints, MakeEndpoint(ingressClusterName, []model.Address{host.SubstanceAddr})) 122 | 123 | listenerName := fmt.Sprintf("listener-%s-%s", ingressClusterName, host.IngressAddr.ListenerSuffix()) 124 | switch service.Protocol { 125 | case model.ProtocolHTTP: 126 | ingressRouteName := "route-" + ingressClusterName 127 | routes = append(routes, MakeRoute(ingressRouteName, ingressClusterName, service.TraceSpan)) 128 | l, err := MakeHTTPListener(&httpListenerParam{ 129 | listenerName: listenerName, 130 | address: &host.IngressAddr, 131 | route: ingressRouteName, 132 | statPrefix: ingressClusterName, 133 | logfileDir: gen.envoyConf.AccessLogDir, 134 | logfileName: ingressClusterName + ".log", 135 | health: NewDisabledHTTPHealthCheck(), 136 | isIngress: true, 137 | traceEnabled: len(service.TraceSpan) > 0, 138 | }) 139 | if err != nil { 140 | return nil, err 141 | } 142 | listeners = append(listeners, l) 143 | case model.ProtocolTCP: 144 | l, err := MakeTCPListener(listenerName, host.IngressAddr, ingressClusterName, ingressClusterName, gen.envoyConf.AccessLogDir, ingressClusterName+".log") 145 | if err != nil { 146 | return nil, err 147 | } 148 | listeners = append(listeners, l) 149 | default: 150 | return nil, fmt.Errorf("%s provides unsupported protocol=%s", service.Name, service.Protocol) 151 | } 152 | 153 | // egress(dependent services) 154 | for depsvc, dephosts := range dependencies { 155 | // integrity checking 156 | ok, ref := service.FindDependentServiceName(depsvc.Name) 157 | if !ok { 158 | return nil, fmt.Errorf("service and its dependencies state are not matched, %s is not found in dependencies", depsvc.Name) 159 | } 160 | 161 | egressClusterName := "egress-" + depsvc.Name 162 | clusters = append(clusters, MakeEDSCluster(egressClusterName, defaultTimeout)) 163 | 164 | depAddresses := make([]model.Address, len(dephosts)) 165 | var i int 166 | for i = 0; i < len(dephosts); i++ { 167 | depAddresses[i] = dephosts[i].IngressAddr 168 | } 169 | endpoints = append(endpoints, MakeEndpoint(egressClusterName, depAddresses)) 170 | 171 | addrstr := fmt.Sprintf("%s:%d", host.EgressHost, ref.EgressPort) 172 | addr, err := model.ParseAddress(addrstr) 173 | if err != nil { 174 | return nil, errors.Wrapf(err, "invalid address of the egress endpoint, svc=%s, host=%s, depend=%s, addr=%s", service.Name, host.Name, depsvc.Name, addrstr) 175 | } 176 | 177 | listenerName := fmt.Sprintf("listener-%s-%s", egressClusterName, addr.ListenerSuffix()) 178 | switch depsvc.Protocol { 179 | case model.ProtocolHTTP: 180 | egressRouteName := "route-" + egressClusterName 181 | routes = append(routes, MakeRoute(egressRouteName, egressClusterName, depsvc.TraceSpan)) 182 | l, err := MakeHTTPListener(&httpListenerParam{ 183 | listenerName: listenerName, 184 | address: addr, 185 | route: egressRouteName, 186 | statPrefix: egressClusterName, 187 | logfileDir: gen.envoyConf.AccessLogDir, 188 | logfileName: egressClusterName + ".log", 189 | health: NewDisabledHTTPHealthCheck(), 190 | isIngress: false, 191 | traceEnabled: len(depsvc.TraceSpan) > 0, 192 | }) 193 | if err != nil { 194 | return nil, err 195 | } 196 | listeners = append(listeners, l) 197 | case model.ProtocolTCP: 198 | l, err := MakeTCPListener(listenerName, *addr, egressClusterName, egressClusterName, gen.envoyConf.AccessLogDir, egressClusterName+".log") 199 | if err != nil { 200 | return nil, err 201 | } 202 | listeners = append(listeners, l) 203 | default: 204 | return nil, fmt.Errorf("%s provides unsupported protocol=%s", service.Name, service.Protocol) 205 | } 206 | } 207 | 208 | snapshot := cache.NewSnapshot(string(version), endpoints, clusters, routes, listeners) 209 | return &snapshot, nil 210 | } 211 | 212 | // latestNodeVersion determines the version of the cache data of the node. It selects the latest from the all related service version. 213 | func (gen *snapGen) latestNodeVersion(service *model.Service, depndencies map[*model.Service][]model.Host) model.Version { 214 | latest := service.Version 215 | for dep := range depndencies { 216 | if gen.versionGen.Compare(latest, dep.Version) > 0 { 217 | latest = dep.Version 218 | } 219 | } 220 | return latest 221 | } 222 | 223 | // MakeEDSCluster creates a EDS cluster. 224 | func MakeEDSCluster(clusterName string, timeout time.Duration) *v2.Cluster { 225 | edsSource := &core.ConfigSource{ 226 | ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{ 227 | ApiConfigSource: &core.ApiConfigSource{ 228 | ApiType: core.ApiConfigSource_GRPC, 229 | ClusterNames: []string{XdsCluster}, 230 | }, 231 | }, 232 | } 233 | 234 | return &v2.Cluster{ 235 | Name: clusterName, 236 | ConnectTimeout: timeout, 237 | Type: v2.Cluster_EDS, 238 | EdsClusterConfig: &v2.Cluster_EdsClusterConfig{ 239 | EdsConfig: edsSource, 240 | }, 241 | } 242 | } 243 | 244 | // MakeEndpoint creates a endpoint on a given address. 245 | func MakeEndpoint(clusterName string, addresses []model.Address) *v2.ClusterLoadAssignment { 246 | endpoints := make([]endpoint.LbEndpoint, len(addresses)) 247 | var i int 248 | for i = 0; i < len(addresses); i++ { 249 | endpoints[i] = endpoint.LbEndpoint{ 250 | Endpoint: &endpoint.Endpoint{ 251 | Address: &core.Address{ 252 | Address: &core.Address_SocketAddress{ 253 | SocketAddress: &core.SocketAddress{ 254 | Protocol: core.TCP, 255 | Address: addresses[i].Hostname, 256 | PortSpecifier: &core.SocketAddress_PortValue{ 257 | PortValue: addresses[i].Port, 258 | }, 259 | }, 260 | }, 261 | }, 262 | }, 263 | } 264 | } 265 | 266 | return &v2.ClusterLoadAssignment{ 267 | ClusterName: clusterName, 268 | Endpoints: []endpoint.LocalityLbEndpoints{{ 269 | LbEndpoints: endpoints, 270 | }}, 271 | } 272 | } 273 | 274 | // MakeRoute creates an HTTP route that routes to a given cluster. 275 | func MakeRoute(routeName, clusterName string, traceSpan string) *v2.RouteConfiguration { 276 | var decorater *route.Decorator 277 | if len(traceSpan) > 0 { 278 | decorater = &route.Decorator{Operation: traceSpan} 279 | } 280 | 281 | return &v2.RouteConfiguration{ 282 | Name: routeName, 283 | VirtualHosts: []route.VirtualHost{{ 284 | Name: routeName, 285 | Domains: []string{"*"}, 286 | Routes: []route.Route{{ 287 | Match: route.RouteMatch{ 288 | PathSpecifier: &route.RouteMatch_Prefix{ 289 | Prefix: "/", 290 | }, 291 | }, 292 | Action: &route.Route_Route{ 293 | Route: &route.RouteAction{ 294 | ClusterSpecifier: &route.RouteAction_Cluster{ 295 | Cluster: clusterName, 296 | }, 297 | }, 298 | }, 299 | Decorator: decorater, 300 | }}, 301 | }}, 302 | } 303 | } 304 | 305 | type httpListenerParam struct { 306 | listenerName string 307 | address *model.Address 308 | route string 309 | statPrefix string 310 | logfileDir string 311 | logfileName string 312 | health *HTTPHealthCheck 313 | isIngress bool 314 | traceEnabled bool 315 | // TODO: more trace settings 316 | } 317 | 318 | // MakeHTTPListener creates a listener using either ADS or RDS for the route. 319 | func MakeHTTPListener(p *httpListenerParam) (*v2.Listener, error) { 320 | // access log service configuration 321 | alsConfig := &accesslog.FileAccessLog{ 322 | Path: p.logfileDir + "/" + p.logfileName, 323 | } 324 | alsConfigPbst, err := util.MessageToStruct(alsConfig) 325 | if err != nil { 326 | return nil, errors.Wrapf(err, "listnere FileAccessLog generation failed(%+v)", *p) 327 | } 328 | 329 | // HTTP filter configuration 330 | httpFilters := []*hcm.HttpFilter{{ 331 | Name: cache.Router, 332 | }} 333 | if p.health.Enabled { 334 | filter, err := p.health.createEnvoyHTTPFilter() 335 | if err != nil { 336 | return nil, err 337 | } 338 | httpFilters = append(httpFilters, filter) 339 | } 340 | 341 | // tracing 342 | var tracing *hcm.HttpConnectionManager_Tracing 343 | if p.traceEnabled { 344 | tracing = &hcm.HttpConnectionManager_Tracing{} 345 | if p.isIngress { 346 | tracing.OperationName = hcm.INGRESS 347 | } else { 348 | tracing.OperationName = hcm.EGRESS 349 | } 350 | } 351 | 352 | // HTTP connection manager configuration 353 | manager := &hcm.HttpConnectionManager{ 354 | CodecType: hcm.AUTO, 355 | StatPrefix: p.statPrefix, 356 | Tracing: tracing, 357 | RouteSpecifier: &hcm.HttpConnectionManager_Rds{ 358 | Rds: &hcm.Rds{ 359 | ConfigSource: core.ConfigSource{ 360 | ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{ 361 | ApiConfigSource: &core.ApiConfigSource{ 362 | ApiType: core.ApiConfigSource_GRPC, 363 | ClusterNames: []string{XdsCluster}, 364 | }, 365 | }, 366 | }, 367 | RouteConfigName: p.route, 368 | }, 369 | }, 370 | HttpFilters: httpFilters, 371 | AccessLog: []*accesslog.AccessLog{{ 372 | Name: "envoy.file_access_log", 373 | Config: alsConfigPbst, 374 | }}, 375 | } 376 | 377 | pbst, err := util.MessageToStruct(manager) 378 | if err != nil { 379 | return nil, errors.Wrapf(err, "listnere Manager generation failed(%+v)", *p) 380 | } 381 | 382 | return &v2.Listener{ 383 | Name: p.listenerName, 384 | Address: core.Address{ 385 | Address: &core.Address_SocketAddress{ 386 | SocketAddress: &core.SocketAddress{ 387 | Protocol: core.TCP, 388 | Address: p.address.Hostname, 389 | PortSpecifier: &core.SocketAddress_PortValue{ 390 | PortValue: p.address.Port, 391 | }, 392 | }, 393 | }, 394 | }, 395 | FilterChains: []listener.FilterChain{{ 396 | Filters: []listener.Filter{{ 397 | Name: cache.HTTPConnectionManager, 398 | Config: pbst, 399 | }}, 400 | }}, 401 | }, nil 402 | } 403 | 404 | // MakeTCPListener creates a TCP listener for a cluster. 405 | func MakeTCPListener(listenerName string, address model.Address, clusterName string, statPrefix string, logfileDir string, logfileName string) (*v2.Listener, error) { 406 | // access log service configuration 407 | alsConfig := &accesslog.FileAccessLog{ 408 | Path: logfileDir + "/" + logfileName, 409 | } 410 | alsConfigPbst, err := util.MessageToStruct(alsConfig) 411 | if err != nil { 412 | return nil, errors.Wrapf(err, "listnere FileAccessLog generation failed(name=%s, cluste=%s, addr=%+v, log=%s:%s)", listenerName, address, clusterName, logfileDir, logfileName) 413 | } 414 | // TCP filter configuration 415 | config := &tcp.TcpProxy{ 416 | StatPrefix: statPrefix, 417 | Cluster: clusterName, 418 | AccessLog: []*accesslog.AccessLog{{ 419 | Name: "envoy.file_access_log", 420 | Config: alsConfigPbst, 421 | }}, 422 | } 423 | pbst, err := util.MessageToStruct(config) 424 | if err != nil { 425 | return nil, errors.Wrapf(err, "tcp proxy generation failed(name=%s, addr=%+v, cluster=%s)", listenerName, address, clusterName) 426 | } 427 | return &v2.Listener{ 428 | Name: listenerName, 429 | Address: core.Address{ 430 | Address: &core.Address_SocketAddress{ 431 | SocketAddress: &core.SocketAddress{ 432 | Protocol: core.TCP, 433 | Address: address.Hostname, 434 | PortSpecifier: &core.SocketAddress_PortValue{ 435 | PortValue: address.Port, 436 | }, 437 | }, 438 | }, 439 | }, 440 | FilterChains: []listener.FilterChain{{ 441 | Filters: []listener.Filter{{ 442 | Name: cache.TCPProxy, 443 | Config: pbst, 444 | }}, 445 | }}, 446 | }, nil 447 | } 448 | -------------------------------------------------------------------------------- /src/core/xds/snapshot_gen_test.go: -------------------------------------------------------------------------------- 1 | package xds 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/envoyproxy/go-control-plane/envoy/api/v2" 8 | "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" 9 | mcore "github.com/rerorero/meshem/src/core" 10 | "github.com/rerorero/meshem/src/model" 11 | "github.com/rerorero/meshem/src/repository" 12 | "github.com/sirupsen/logrus" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func addr2str(addr *core.Address) string { 17 | return fmt.Sprintf("%s:%d", addr.GetSocketAddress().GetAddress(), addr.GetSocketAddress().GetPortValue()) 18 | } 19 | 20 | func TestMakeSnapshot(t *testing.T) { 21 | repo := repository.NewInventoryHeap() 22 | gen := mcore.NewCurrentTimeGenerator() 23 | inventory := mcore.NewInventoryService(repo, nil, gen, logrus.New()) 24 | conf := model.EnvoyConf{ 25 | ClusterTimeoutMS: 2000, 26 | AccessLogDir: "/var/log/test", 27 | } 28 | sut := NewSnapshotGen(inventory, logrus.New(), gen, conf) 29 | 30 | // register service 31 | svcA := &model.Service{ 32 | Name: "serviceA", 33 | Version: "1", 34 | Protocol: model.ProtocolTCP, 35 | } 36 | svcB := &model.Service{ 37 | Name: "serviceB", 38 | Version: "1", 39 | Protocol: model.ProtocolHTTP, 40 | } 41 | allsvc := []*model.Service{svcA, svcB} 42 | 43 | var firstVer model.Version 44 | for _, svc := range allsvc { 45 | s, err := inventory.RegisterService(svc.Name, svc.Protocol) 46 | assert.NoError(t, err) 47 | firstVer = s.Version 48 | } 49 | a2bport := uint32(10001) 50 | assert.NoError(t, inventory.AddServiceDependency(svcA.Name, svcB.Name, a2bport)) 51 | 52 | // register host 53 | svcA1, err := inventory.RegisterHost(svcA.Name, "svcA1", "192.168.0.1:80", "127.0.0.1:8001", "127.0.0.1") 54 | assert.NoError(t, err) 55 | _, err = inventory.RegisterHost(svcB.Name, "svcB1", "192.168.1.1:8080", "127.0.0.1:9001", "127.0.0.1") 56 | assert.NoError(t, err) 57 | _, err = inventory.RegisterHost(svcB.Name, "svcB2", "192.168.1.2:8080", "127.0.0.1:9002", "127.0.0.1") 58 | assert.NoError(t, err) 59 | 60 | // make snapshot 61 | shotA, err := sut.MakeSnapshotsOfService(svcA.Name) 62 | assert.NoError(t, err) 63 | assert.Equal(t, 1, len(shotA)) 64 | actualA1, ok := FindSnapshotByName(shotA, svcA1.Name) 65 | assert.True(t, ok) 66 | // listeneres 67 | assert.NotEqual(t, actualA1.Listeners.Version, firstVer) 68 | assert.Equal(t, 2, len(actualA1.Listeners.Items)) 69 | for _, item := range actualA1.Listeners.Items { 70 | listnere := item.(*v2.Listener) 71 | switch listnere.Name { 72 | case "listener-ingress-19216801-80": 73 | assert.Equal(t, "192.168.0.1:80", addr2str(&listnere.Address)) 74 | case "listener-egress-serviceB-127001-10001": 75 | assert.Equal(t, "127.0.0.1:10001", addr2str(&listnere.Address)) 76 | default: 77 | assert.Failf(t, "unknown listenere name: %s", listnere.Name) 78 | } 79 | } 80 | // clusters 81 | assert.Equal(t, 2, len(actualA1.Clusters.Items)) 82 | // routes 83 | assert.Equal(t, 1, len(actualA1.Routes.Items)) 84 | // endpoints 85 | assert.Equal(t, 2, len(actualA1.Endpoints.Items)) 86 | ingressCluster := []*v2.ClusterLoadAssignment{} 87 | egressBCluster := []*v2.ClusterLoadAssignment{} 88 | for _, item := range actualA1.Endpoints.Items { 89 | i := item.(*v2.ClusterLoadAssignment) 90 | switch i.ClusterName { 91 | case "ingress": 92 | ingressCluster = append(ingressCluster, i) 93 | case "egress-serviceB": 94 | egressBCluster = append(egressBCluster, i) 95 | default: 96 | assert.Failf(t, "invalid cluster: %s", i.ClusterName) 97 | } 98 | } 99 | // ingress 100 | assert.Equal(t, 1, len(ingressCluster)) 101 | assert.Equal(t, "127.0.0.1:8001", addr2str(ingressCluster[0].Endpoints[0].LbEndpoints[0].Endpoint.Address)) 102 | // egress 103 | assert.Equal(t, 1, len(egressBCluster)) 104 | assert.Equal(t, 2, len(egressBCluster[0].Endpoints[0].LbEndpoints)) 105 | egressBAddress := []string{} 106 | for _, e := range egressBCluster[0].Endpoints[0].LbEndpoints { 107 | egressBAddress = append(egressBAddress, addr2str(e.Endpoint.Address)) 108 | } 109 | assert.ElementsMatch(t, []string{"192.168.1.1:8080", "192.168.1.2:8080"}, egressBAddress) 110 | } 111 | -------------------------------------------------------------------------------- /src/meshem/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "net/url" 7 | "os" 8 | 9 | "github.com/rerorero/meshem/src" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/rerorero/meshem/src/core" 13 | "github.com/rerorero/meshem/src/core/ctlapi" 14 | "github.com/rerorero/meshem/src/core/xds" 15 | "github.com/rerorero/meshem/src/repository" 16 | "github.com/rerorero/meshem/src/utils" 17 | 18 | "github.com/rerorero/meshem/src/model" 19 | "github.com/sirupsen/logrus" 20 | ) 21 | 22 | var ( 23 | confPath = flag.String("conf.file", "/etc/meshem.yaml", "Path to configuration file.") 24 | logger = logrus.New() 25 | ) 26 | 27 | func main() { 28 | flag.Parse() 29 | ctx := context.Background() 30 | 31 | logger.Infof("meshem server version=%s", src.MeshemVersion()) 32 | 33 | // read config file 34 | conf, err := model.NewMeshemConfFile(*confPath) 35 | if err != nil { 36 | ExitError(errors.Wrapf(err, "failed to read config file: %s", *confPath)) 37 | } 38 | 39 | // consul 40 | consul, err := newConsulFromConf(&conf.Consul) 41 | if err != nil { 42 | ExitError(err) 43 | } 44 | 45 | // inventory repository 46 | inventoryRepo := repository.NewInventoryConsul(consul) 47 | 48 | // service discovery repository 49 | var discoveryRepo repository.DiscoveryRepository 50 | if conf.Discovery != nil { 51 | switch conf.Discovery.Type { 52 | case model.DiscoveryTypeConsul: 53 | discoveryConsul, err := newConsulFromConf(conf.Discovery.Consul) 54 | if err != nil { 55 | ExitError(err) 56 | } 57 | discoveryRepo = repository.NewDiscoveryConsul(discoveryConsul, repository.DefaultGlobalServiceName) 58 | } 59 | } 60 | 61 | // inventory 62 | versionGen := core.NewCurrentTimeGenerator() 63 | inventoryService := core.NewInventoryService(inventoryRepo, discoveryRepo, versionGen, logger) 64 | xdsServer := xds.NewXDSServer(inventoryService, versionGen, *conf, ctx, logger) 65 | 66 | // start control api server 67 | apiServer := ctlapi.NewServer(inventoryService, conf.CtlAPI, logger) 68 | err = apiServer.Run() 69 | if err != nil { 70 | ExitError(errors.Wrap(err, "failed to strat control API server")) 71 | } 72 | 73 | // start snapshot collector 74 | xdsServer.RunSnapshotCollector() 75 | 76 | // start xds server 77 | grpcServer, err := xdsServer.RunXDS() 78 | if err != nil { 79 | ExitError(errors.Wrap(err, "failed to run xds server.")) 80 | } 81 | 82 | <-ctx.Done() 83 | grpcServer.GracefulStop() 84 | logger.Info("xds shutdown") 85 | 86 | os.Exit(0) 87 | } 88 | 89 | // ExitError exits on error 90 | func ExitError(err error) { 91 | logger.Error(err) 92 | os.Exit(1) 93 | } 94 | 95 | func newConsulFromConf(conf *model.ConsulConf) (*utils.Consul, error) { 96 | consulURL, err := url.Parse(conf.URL) 97 | if err != nil { 98 | return nil, errors.Wrapf(err, "invalid consul url: %s", conf.URL) 99 | } 100 | consul, err := utils.NewConsul(consulURL, conf.Token, conf.Datacenter) 101 | if err != nil { 102 | return nil, errors.Wrap(err, "failed to initialize consul") 103 | } 104 | return consul, nil 105 | } 106 | -------------------------------------------------------------------------------- /src/meshemctl/command/apiclient.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/rerorero/meshem/src/core/ctlapi" 11 | "github.com/rerorero/meshem/src/model" 12 | ) 13 | 14 | // NewAPIClient creates a new APIClient inscance. 15 | func NewAPIClient() (*ctlapi.APIClient, error) { 16 | endpoint := os.Getenv("MESHEM_CTLAPI_ENDPOINT") 17 | if len(endpoint) == 0 { 18 | endpoint = fmt.Sprintf("http://127.0.0.1:%d", model.DefaultCtrlAPIPort) 19 | } 20 | 21 | timeout := os.Getenv("MESHEM_CTLAPI_TIMEOUT") 22 | if len(timeout) == 0 { 23 | timeout = "60" 24 | } 25 | t, err := strconv.Atoi(timeout) 26 | if err != nil { 27 | ExitWithError(errors.New("MESHEM_CTLAPI_TIMEOUT must be a number")) 28 | } 29 | timeoutDuration := time.Duration(t) * time.Second 30 | 31 | return ctlapi.NewClient(endpoint, timeoutDuration) 32 | } 33 | -------------------------------------------------------------------------------- /src/meshemctl/command/error.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func ExitWithError(err error) { 9 | fmt.Fprintln(os.Stderr, "Error: ", err) 10 | os.Exit(1) 11 | } 12 | -------------------------------------------------------------------------------- /src/meshemctl/command/svc_cmd.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/rerorero/meshem/src/model" 10 | "github.com/spf13/cobra" 11 | yaml "gopkg.in/yaml.v2" 12 | ) 13 | 14 | var ( 15 | filePath string 16 | ) 17 | 18 | // NewServiceCommand returns the command object for 'svc'. 19 | func NewServiceCommand() *cobra.Command { 20 | cmd := &cobra.Command{ 21 | Use: "svc ", 22 | Short: "Service related commands", 23 | } 24 | cmd.AddCommand(newApplyServiceCommand()) 25 | return cmd 26 | } 27 | 28 | func newApplyServiceCommand() *cobra.Command { 29 | cmd := &cobra.Command{ 30 | Use: "apply -f ", 31 | Short: "Apply a configuration to a service by filename", 32 | Run: applyService, 33 | } 34 | cmd.Flags().StringVarP(&filePath, "filepath", "f", "", "(required) File path that defines the service") 35 | return cmd 36 | } 37 | 38 | func applyService(cmd *cobra.Command, args []string) { 39 | if len(args) != 1 { 40 | ExitWithError(errors.New("command needs an argument as service name")) 41 | } 42 | serviceName := args[0] 43 | if len(filePath) == 0 { 44 | ExitWithError(fmt.Errorf("command needs --filepath argument")) 45 | } 46 | 47 | buf, err := ioutil.ReadFile(filePath) 48 | if err != nil { 49 | ExitWithError(errors.Wrapf(err, "could not read resource file(%s)", filePath)) 50 | } 51 | 52 | var param model.IdempotentServiceParam 53 | err = yaml.Unmarshal(buf, ¶m) 54 | if err != nil { 55 | ExitWithError(errors.Wrapf(err, "failed to parse resource file(%s)", filePath)) 56 | } 57 | 58 | client, err := NewAPIClient() 59 | if err != nil { 60 | ExitWithError(err) 61 | } 62 | 63 | resp, _, err := client.PutService(serviceName, param) 64 | if err != nil { 65 | ExitWithError(err) 66 | } 67 | 68 | fmt.Printf("OK (Changed=%t)\n", resp.Changed) 69 | } 70 | 71 | func showService(cmd *cobra.Command, args []string) { 72 | if len(args) != 1 { 73 | ExitWithError(errors.New("command needs an argument as service name")) 74 | } 75 | serviceName := args[0] 76 | 77 | client, err := NewAPIClient() 78 | if err != nil { 79 | ExitWithError(err) 80 | } 81 | 82 | resp, _, err := client.GetService(serviceName) 83 | if err != nil { 84 | ExitWithError(err) 85 | } 86 | byte, err := json.MarshalIndent(resp, "", " ") 87 | if err != nil { 88 | ExitWithError(errors.Wrapf(err, "failed to parse the response as JSON: %+v", resp)) 89 | } 90 | 91 | fmt.Println(string(byte)) 92 | } 93 | -------------------------------------------------------------------------------- /src/meshemctl/command/version_cmd.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rerorero/meshem/src" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func NewVersionCommand() *cobra.Command { 11 | return &cobra.Command{ 12 | Use: "version", 13 | Short: "Prints the version of meshem", 14 | Run: versionCommandFunc, 15 | } 16 | } 17 | 18 | func versionCommandFunc(cmd *cobra.Command, args []string) { 19 | fmt.Println("meshemctl version:", src.MeshemVersion()) 20 | } 21 | -------------------------------------------------------------------------------- /src/meshemctl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/rerorero/meshem/src/meshemctl/command" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var ( 9 | rootCmd = &cobra.Command{ 10 | Use: "meshemctl", 11 | Short: "meshem is an example implementation of service mesh.", 12 | } 13 | ) 14 | 15 | func init() { 16 | rootCmd.AddCommand(command.NewVersionCommand()) 17 | rootCmd.AddCommand(command.NewServiceCommand()) 18 | } 19 | 20 | func main() { 21 | if err := rootCmd.Execute(); err != nil { 22 | command.ExitWithError(err) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/model/address.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type Address struct { 12 | Hostname string `json:"host" yaml:"host"` 13 | Port uint32 `json:"port" yaml:"port"` 14 | } 15 | 16 | func (addr *Address) String() string { 17 | return fmt.Sprintf("%s:%d", addr.Hostname, addr.Port) 18 | } 19 | 20 | func ParseAddress(s string) (*Address, error) { 21 | pair := strings.Split(s, ":") 22 | if len(pair) != 2 { 23 | return nil, fmt.Errorf("Invalid Address: %s", s) 24 | } 25 | port, err := strconv.Atoi(pair[1]) 26 | if err != nil { 27 | return nil, errors.Wrapf(err, "Invalid port address: %s", s) 28 | } 29 | 30 | return &Address{pair[0], uint32(port)}, nil 31 | } 32 | 33 | // TODO: replaced with hash? 34 | func (addr *Address) ListenerSuffix() string { 35 | return fmt.Sprintf("%s-%d", strings.Replace(addr.Hostname, ".", "", -1), addr.Port) 36 | } 37 | -------------------------------------------------------------------------------- /src/model/address_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestParseAddress(t *testing.T) { 10 | addr, err := ParseAddress("1.2.3.4:5678") 11 | assert.NoError(t, err) 12 | assert.Equal(t, addr, &Address{"1.2.3.4", 5678}) 13 | 14 | _, err = ParseAddress("1.2.3.4") 15 | assert.Error(t, err) 16 | 17 | _, err = ParseAddress("1.2:domain:80") 18 | assert.Error(t, err) 19 | 20 | _, err = ParseAddress("1.2.3.4:f") 21 | assert.Error(t, err) 22 | } 23 | -------------------------------------------------------------------------------- /src/model/config.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "github.com/pkg/errors" 8 | yaml "gopkg.in/yaml.v2" 9 | ) 10 | 11 | // EnvoyConf relates to envoy and xds. 12 | type EnvoyConf struct { 13 | ClusterTimeoutMS int `yaml:"cluster_timeout_ms,omitempty"` 14 | AccessLogDir string `yaml:"access_log_dir"` 15 | } 16 | 17 | // XDSConf relates to xds function. 18 | type XDSConf struct { 19 | Port uint32 `yaml:"port,omitempty"` 20 | CacheCollectionIntervalMS int `yaml:"cache_collection_interval_ms,omitempty"` 21 | IsADSMode bool `yaml:"ads_mode,omitempty"` 22 | } 23 | 24 | // ConsulConf relates to consul. 25 | type ConsulConf struct { 26 | URL string `yaml:"url"` 27 | Token string `yaml:"token"` 28 | Datacenter string `yaml:"datacenter,omitempty"` 29 | } 30 | 31 | // CtlAPIConf relates to meshem control api. 32 | type CtlAPIConf struct { 33 | Port uint32 `yaml:"port"` 34 | } 35 | 36 | // DiscoveryConf relates to discovery service. This is optional 37 | type DiscoveryConf struct { 38 | Type string `yaml:"type"` 39 | Consul *ConsulConf `yaml:"consul,omitempty"` 40 | } 41 | 42 | // MeshemConf is configurations for conductor server. 43 | type MeshemConf struct { 44 | Envoy EnvoyConf `yaml:"envoy"` 45 | XDS XDSConf `yaml:"xds"` 46 | Consul ConsulConf `yaml:"consul"` 47 | CtlAPI CtlAPIConf `yaml:"ctlapi"` 48 | Discovery *DiscoveryConf `yaml:"discovery"` 49 | } 50 | 51 | const ( 52 | // DefaultXDSPort is default port for XDS. 53 | DefaultXDSPort = 8090 54 | // DefaultCtrlAPIPort is default port for the control API. 55 | DefaultCtrlAPIPort = 8091 56 | // DiscoveryTypeConsul is set to use consul discovery service 57 | DiscoveryTypeConsul = "consul" 58 | ) 59 | 60 | // NewMeshemConfFile parses configuration file. 61 | func NewMeshemConfFile(confPath string) (*MeshemConf, error) { 62 | buf, err := ioutil.ReadFile(confPath) 63 | if err != nil { 64 | return nil, fmt.Errorf("could not read config - %s", err) 65 | } 66 | 67 | return NewMeshemConfYaml(buf) 68 | } 69 | 70 | // NewMeshemConfYaml parses configuration YAML string. 71 | func NewMeshemConfYaml(yamlbytes []byte) (*MeshemConf, error) { 72 | conf := &MeshemConf{} 73 | err := yaml.Unmarshal(yamlbytes, conf) 74 | if err != nil { 75 | return nil, errors.Wrap(err, fmt.Sprintf("Fatal: Could not parse config(%s)", string(yamlbytes))) 76 | } 77 | 78 | // default values 79 | if conf.Envoy.ClusterTimeoutMS == 0 { 80 | conf.Envoy.ClusterTimeoutMS = 5000 81 | } 82 | if len(conf.Envoy.AccessLogDir) == 0 { 83 | conf.Envoy.AccessLogDir = "/var/log/envoy" 84 | } 85 | if conf.XDS.Port == 0 { 86 | conf.XDS.Port = DefaultXDSPort 87 | } 88 | if conf.XDS.CacheCollectionIntervalMS == 0 { 89 | conf.XDS.CacheCollectionIntervalMS = 10000 90 | } 91 | if len(conf.Consul.Datacenter) == 0 { 92 | conf.Consul.Datacenter = "dc1" 93 | } 94 | if conf.CtlAPI.Port == 0 { 95 | conf.CtlAPI.Port = DefaultCtrlAPIPort 96 | } 97 | 98 | // validation 99 | if conf.Discovery != nil { 100 | switch conf.Discovery.Type { 101 | case DiscoveryTypeConsul: 102 | if conf.Discovery.Consul == nil { 103 | return nil, fmt.Errorf("discovery.consul should be set when consul discovery is enabled") 104 | } 105 | if len(conf.Discovery.Consul.Datacenter) == 0 { 106 | conf.Discovery.Consul.Datacenter = "dc1" 107 | } 108 | default: 109 | return nil, fmt.Errorf("invalid discovery type: %s", conf.Discovery.Type) 110 | } 111 | } 112 | 113 | return conf, nil 114 | } 115 | -------------------------------------------------------------------------------- /src/model/host.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | // Host contains information of an user managed host. 11 | type Host struct { 12 | Name string `json:"name" yaml:"name"` 13 | IngressAddr Address `json:"ingressAddr" yaml:"ingressAddr"` 14 | SubstanceAddr Address `json:"substanceAddr" yaml:"substanceAddr"` 15 | EgressHost string `json:"egressHost" yaml:"egressHost"` 16 | // TODO: add an admin port (IngressAddr + admin port(8001)) 17 | // AdminAddr Address `json:"adminAddr" yaml:"adminAddr"` 18 | } 19 | 20 | const ( 21 | // DefaultAdminPort is default value of envoy admin port 22 | DefaultAdminPort = 8001 23 | ) 24 | 25 | var ( 26 | rHostName = regexp.MustCompile(`^[A-Za-z0-9_\-]{1,64}$`) 27 | ) 28 | 29 | // NewHost creates a new Host instance. 30 | func NewHost(name string, ingresAddress string, substanceAddress string, egressHost string) (host Host, err error) { 31 | ingress, err := ParseAddress(ingresAddress) 32 | if err != nil { 33 | return host, err 34 | } 35 | 36 | substance, err := ParseAddress(substanceAddress) 37 | if err != nil { 38 | return host, err 39 | } 40 | 41 | return Host{ 42 | Name: name, 43 | IngressAddr: *ingress, 44 | SubstanceAddr: *substance, 45 | EgressHost: egressHost, 46 | }, nil 47 | } 48 | 49 | // Validate checks that the host is valid. 50 | func (h *Host) Validate() error { 51 | err := validateHostname(h.Name) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | if h.IngressAddr == h.SubstanceAddr { 57 | return fmt.Errorf("duplicate ingress port and egress port (host=%s, addr=%s)", h.Name, h.IngressAddr.String()) 58 | } 59 | 60 | if strings.Contains(h.EgressHost, ":") { 61 | return fmt.Errorf("egrsshost can not contain port number: host=%s, egress=%s", h.Name, h.EgressHost) 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // Update updates the host attributes. 68 | func (h *Host) Update(ingresAddress *string, substanceAddress *string, egressHost *string) (err error) { 69 | ingress := h.IngressAddr.String() 70 | if ingresAddress != nil { 71 | ingress = *ingresAddress 72 | } 73 | substance := h.SubstanceAddr.String() 74 | if substanceAddress != nil { 75 | substance = *substanceAddress 76 | } 77 | egress := h.EgressHost 78 | if egressHost != nil { 79 | egress = *egressHost 80 | } 81 | 82 | *h, err = NewHost(h.Name, ingress, substance, egress) 83 | return err 84 | } 85 | 86 | // GetAdminAddr returns envoy's admin endpoint. 87 | func (h *Host) GetAdminAddr() *Address { 88 | // TODO: get from property 89 | return &Address{ 90 | Hostname: h.IngressAddr.Hostname, 91 | Port: DefaultAdminPort, 92 | } 93 | } 94 | 95 | func validateHostname(s string) error { 96 | if !rHostName.MatchString(s) { 97 | return errors.New("hostname must consist of alphanumeric characters, underscores and dashes, and less than 64 characters") 98 | } 99 | return nil 100 | } 101 | 102 | // FilterHosts filters a slice of Host. 103 | func FilterHosts(hosts []*Host, pred func(*Host) bool) (filtered []*Host) { 104 | for _, host := range hosts { 105 | if pred(host) { 106 | filtered = append(filtered, host) 107 | } 108 | } 109 | return filtered 110 | } 111 | 112 | // MapHostsToString transforms a slice of Host to a slice of string. 113 | func MapHostsToString(hosts []*Host, f func(*Host) string) (mapped []string) { 114 | for _, host := range hosts { 115 | mapped = append(mapped, f(host)) 116 | } 117 | return mapped 118 | } 119 | -------------------------------------------------------------------------------- /src/model/host_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestHostValidate(t *testing.T) { 10 | host, err := NewHost("valid-01_32", "192.168.0.1:1234", "127.0.0.1:5678", "127.0.0.1") 11 | assert.NoError(t, err) 12 | assert.NoError(t, host.Validate()) 13 | 14 | // invalid hostname 15 | host.Name = "ivalid.aaa" 16 | assert.Error(t, host.Validate()) 17 | host.Name = "ivalidddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" 18 | assert.Error(t, host.Validate()) 19 | 20 | // duplicate port 21 | host2, err := NewHost("valid-01_32", "192.168.0.1:1234", "192.168.0.1:1234", "127.0.0.1") 22 | assert.NoError(t, err) 23 | assert.Error(t, host2.Validate()) 24 | } 25 | 26 | func TestUpdate(t *testing.T) { 27 | host, err := NewHost("name", "192.168.0.1:1234", "192.168.0.1:5678", "127.0.0.1") 28 | assert.NoError(t, err) 29 | assert.NoError(t, host.Validate()) 30 | 31 | newIng := "192.168.0.1:9090" 32 | newSub := "192.168.0.1:8080" 33 | expect, err := NewHost("name", newIng, newSub, "127.0.0.1") 34 | assert.NoError(t, err) 35 | err = host.Update(&newIng, &newSub, nil) 36 | assert.NoError(t, err) 37 | assert.Equal(t, expect, host) 38 | 39 | newEgress := "192.168.0.2" 40 | err = host.Update(nil, nil, &newEgress) 41 | assert.NoError(t, err) 42 | expect, err = NewHost("name", newIng, newSub, newEgress) 43 | assert.NoError(t, err) 44 | assert.Equal(t, expect, host) 45 | } 46 | -------------------------------------------------------------------------------- /src/model/service.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "regexp" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/rerorero/meshem/src/utils" 12 | ) 13 | 14 | // DependentService contains service and port 15 | type DependentService struct { 16 | Name string `json:"name" yaml:"name"` 17 | EgressPort uint32 `json:"egressPort" yaml:"egressPort"` 18 | } 19 | 20 | // Service contains information of user service. 21 | // TODO: be able to configure more flexible routes. 22 | type Service struct { 23 | Name string `json:"name" yaml:"name"` 24 | HostNames []string `json:"hostNames" yaml:"hostNames"` 25 | DependentServices []DependentService `json:"dependentServices" yaml:"dependentServices"` 26 | Protocol string `json:"protocol" yaml:"protocol"` 27 | TraceSpan string `json:"trace_sapn" yaml:"trace_span"` 28 | Version Version `json:"version" yaml:"version"` 29 | } 30 | 31 | // IdempotentServiceParam is used as a parameter by updating idempotently 32 | type IdempotentServiceParam struct { 33 | Protocol string `json:"protocol" yaml:"protocol"` 34 | Hosts []Host `json:"hosts" yaml:"hosts"` 35 | DependentServices []DependentService `json:"dependentServices" yaml:"dependentServices"` 36 | } 37 | 38 | const ( 39 | // ProtocolHTTP is for HTTP service 40 | ProtocolHTTP = "HTTP" 41 | // ProtocolTCP is for TCP service 42 | ProtocolTCP = "TCP" 43 | ) 44 | 45 | var ( 46 | rServiceName = regexp.MustCompile(`^[A-Za-z0-9_\-]{1,64}$`) 47 | allProtocol = []string{ProtocolHTTP, ProtocolTCP} 48 | ) 49 | 50 | // NewService creates a new service instance. 51 | func NewService(name string, protocol string) Service { 52 | // currently trace span is same as service name. 53 | return Service{Name: name, Protocol: protocol, TraceSpan: name} 54 | } 55 | 56 | // Validate checks Service object format. 57 | func (s *Service) Validate() error { 58 | err := validateServiceName(s.Name) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | for _, h := range s.HostNames { 64 | err := validateHostname(h) 65 | if err != nil { 66 | return err 67 | } 68 | } 69 | 70 | // check the service names and port duplicates 71 | ports := map[uint32]string{} 72 | svcNames := map[string]bool{} 73 | var i int 74 | for i = 0; i < len(s.DependentServices); i++ { 75 | err := validateServiceName(s.DependentServices[i].Name) 76 | if err != nil { 77 | return err 78 | } 79 | if s.DependentServices[i].EgressPort == 0 { 80 | return fmt.Errorf("invalid egress port number(%d) of service=%s", s.DependentServices[i].EgressPort, s.DependentServices[i].Name) 81 | } 82 | if s2, ok := ports[s.DependentServices[i].EgressPort]; ok { 83 | return fmt.Errorf("duplicate service port=%d used for %s and %s", s.DependentServices[i].EgressPort, s.DependentServices[i].Name, s2) 84 | } 85 | if _, ok := svcNames[s.DependentServices[i].Name]; ok { 86 | return fmt.Errorf("duplicate dependent service names: %s", s.DependentServices[i].Name) 87 | } 88 | svcNames[s.DependentServices[i].Name] = true 89 | ports[s.DependentServices[i].EgressPort] = s.DependentServices[i].Name 90 | } 91 | 92 | _, ok := utils.ContainsString(allProtocol, s.Protocol) 93 | if !ok { 94 | return fmt.Errorf("%s is invalid protocol", s.Protocol) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | // AppendDependent appends a new dpendent service. 101 | func (s *Service) AppendDependent(dependent DependentService) error { 102 | // check dupclicates 103 | if ok, dup := s.FindDependentServicePort(dependent.EgressPort); ok { 104 | return fmt.Errorf("the port=%d is already used by %s", dependent.EgressPort, dup.Name) 105 | } 106 | if ok, _ := s.FindDependentServiceName(dependent.Name); ok { 107 | return fmt.Errorf("the service=%s is already referenced by %s", dependent.Name, s.Name) 108 | } 109 | if dependent.EgressPort == 0 { 110 | return fmt.Errorf("invalid egress port number(%d) of service=%s", dependent.EgressPort, dependent.Name) 111 | } 112 | s.DependentServices = append(s.DependentServices, dependent) 113 | return nil 114 | } 115 | 116 | // RemoveDependent removes a dependent service. 117 | func (s *Service) RemoveDependent(name string) bool { 118 | updated := false 119 | services := []DependentService{} 120 | var i int 121 | for i = 0; i < len(s.DependentServices); i++ { 122 | if s.DependentServices[i].Name == name { 123 | updated = true 124 | } else { 125 | services = append(services, s.DependentServices[i]) 126 | } 127 | } 128 | s.DependentServices = services 129 | return updated 130 | } 131 | 132 | // FindDependentServicePort finds a dependent service which uses the port. 133 | func (s *Service) FindDependentServicePort(port uint32) (bool, *DependentService) { 134 | var i int 135 | for i = 0; i < len(s.DependentServices); i++ { 136 | if s.DependentServices[i].EgressPort == port { 137 | return true, &s.DependentServices[i] 138 | } 139 | } 140 | return false, nil 141 | } 142 | 143 | // FindDependentServiceName finds a dependent service. 144 | func (s *Service) FindDependentServiceName(name string) (bool, *DependentService) { 145 | var i int 146 | for i = 0; i < len(s.DependentServices); i++ { 147 | if s.DependentServices[i].Name == name { 148 | return true, &s.DependentServices[i] 149 | } 150 | } 151 | return false, nil 152 | } 153 | 154 | // DependentServiceNames returns dependent service names. 155 | func (s *Service) DependentServiceNames() []string { 156 | names := make([]string, len(s.DependentServices)) 157 | var i int 158 | for i = 0; i < len(s.DependentServices); i++ { 159 | names[i] = s.DependentServices[i].Name 160 | } 161 | return names 162 | } 163 | 164 | func compareServiceDependent(l *DependentService, r *DependentService) int { 165 | if l.Name != r.Name { 166 | return strings.Compare(l.Name, r.Name) 167 | } 168 | return int(r.EgressPort) - int(l.EgressPort) 169 | } 170 | 171 | // EqualsServiceDependencies compares two DependentService slices. 172 | func EqualsServiceDependencies(l []DependentService, r []DependentService) bool { 173 | if len(l) != len(r) { 174 | return false 175 | } 176 | sort.Slice(l, func(i, j int) bool { 177 | return compareServiceDependent(&l[i], &l[j]) > 0 178 | }) 179 | sort.Slice(r, func(i, j int) bool { 180 | return compareServiceDependent(&r[i], &r[j]) > 0 181 | }) 182 | var i int 183 | for i = 0; i < len(l); i++ { 184 | if !reflect.DeepEqual(l[i], r[i]) { 185 | return false 186 | } 187 | } 188 | return true 189 | } 190 | 191 | func validateServiceName(s string) error { 192 | if !rServiceName.MatchString(s) { 193 | return errors.New("service name must consist of alphanumeric characters, underscores and dashes, and less than 64 characters") 194 | } 195 | return nil 196 | } 197 | 198 | // FilterServices filteres a slice of service via prediction function. 199 | func FilterServices(services []*Service, pred func(*Service) bool) (filtered []*Service) { 200 | for _, svc := range services { 201 | if pred(svc) { 202 | filtered = append(filtered, svc) 203 | } 204 | } 205 | return filtered 206 | } 207 | 208 | // MapServicesToString transforms a slice of service to a string slice. 209 | func MapServicesToString(services []*Service, f func(*Service) string) (mapped []string) { 210 | for _, host := range services { 211 | mapped = append(mapped, f(host)) 212 | } 213 | return mapped 214 | } 215 | 216 | // NewService creates a Service object from an IdempontentServiceParam. 217 | func (param *IdempotentServiceParam) NewService(name string) Service { 218 | hostnames := make([]string, len(param.Hosts)) 219 | var i int 220 | for i = 0; i < len(param.Hosts); i++ { 221 | hostnames[i] = param.Hosts[i].Name 222 | } 223 | // currently trace span is same as service name. 224 | return Service{ 225 | Name: name, 226 | HostNames: hostnames, 227 | DependentServices: param.DependentServices, 228 | Protocol: param.Protocol, 229 | TraceSpan: name, 230 | } 231 | } 232 | 233 | // NewIdempotentService creates an IdempotentServiceParam object from a service and its hosts. 234 | func NewIdempotentService(svc *Service, hosts []Host) IdempotentServiceParam { 235 | return IdempotentServiceParam{ 236 | Protocol: svc.Protocol, 237 | Hosts: hosts, 238 | DependentServices: svc.DependentServices, 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/model/service_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestServiceValidate(t *testing.T) { 10 | depends := []DependentService{ 11 | DependentService{ 12 | Name: "valid1", 13 | EgressPort: 9001, 14 | }, 15 | DependentService{ 16 | Name: "valid2", 17 | EgressPort: 9002, 18 | }, 19 | } 20 | s := Service{ 21 | Name: "service", 22 | HostNames: []string{"valid"}, 23 | Protocol: ProtocolHTTP, 24 | DependentServices: depends, 25 | } 26 | 27 | s.Name = "valid-01_32" 28 | assert.NoError(t, s.Validate()) 29 | 30 | // invalid service name 31 | s.Name = "ivalid.aaa" 32 | assert.Error(t, s.Validate()) 33 | 34 | // invalid host names 35 | s.Name = "valid" 36 | s.HostNames = append(s.HostNames, "in.valid") 37 | assert.Error(t, s.Validate()) 38 | 39 | // invalid protocol 40 | s.HostNames = []string{"valid"} 41 | s.Protocol = "invalid" 42 | assert.Error(t, s.Validate()) 43 | 44 | // duplicated service names 45 | s.Protocol = ProtocolHTTP 46 | err := s.AppendDependent(DependentService{Name: "valid1", EgressPort: 9003}) 47 | assert.Error(t, err) 48 | s.DependentServices = append(depends, DependentService{Name: "valid1", EgressPort: 9003}) 49 | assert.Error(t, s.Validate()) 50 | 51 | // duplicated service port 52 | s.DependentServices = depends 53 | err = s.AppendDependent(DependentService{Name: "valid3", EgressPort: 9001}) 54 | assert.Error(t, err) 55 | s.DependentServices = append(depends, DependentService{Name: "valid3", EgressPort: 9001}) 56 | assert.Error(t, s.Validate()) 57 | 58 | // invalid egress port 59 | s.DependentServices = depends 60 | err = s.AppendDependent(DependentService{Name: "valid3"}) 61 | assert.Error(t, err) 62 | s.DependentServices = append(depends, DependentService{Name: "valid3"}) 63 | assert.Error(t, s.Validate()) 64 | 65 | // can append 66 | s.DependentServices = depends 67 | err = s.AppendDependent(DependentService{Name: "valid3", EgressPort: 9003}) 68 | assert.NoError(t, err) 69 | assert.Equal(t, len(depends)+1, len(s.DependentServices)) 70 | 71 | // can remove 72 | ok := s.RemoveDependent("valid3") 73 | assert.NoError(t, err) 74 | assert.True(t, ok) 75 | assert.Equal(t, len(depends), len(s.DependentServices)) 76 | } 77 | 78 | func TestEqualsSserviceDependencies(t *testing.T) { 79 | a := []DependentService{ 80 | DependentService{ 81 | Name: "abc", 82 | EgressPort: 9002, 83 | }, 84 | DependentService{ 85 | Name: "ab", 86 | EgressPort: 9001, 87 | }, 88 | DependentService{ 89 | Name: "abcd", 90 | EgressPort: 9003, 91 | }, 92 | } 93 | sameA := []DependentService{ 94 | DependentService{ 95 | Name: "abcd", 96 | EgressPort: 9003, 97 | }, 98 | DependentService{ 99 | Name: "ab", 100 | EgressPort: 9001, 101 | }, 102 | DependentService{ 103 | Name: "abc", 104 | EgressPort: 9002, 105 | }, 106 | } 107 | b := []DependentService{ 108 | DependentService{ 109 | Name: "abcd", 110 | EgressPort: 9003, 111 | }, 112 | DependentService{ 113 | Name: "abc", 114 | EgressPort: 9002, 115 | }, 116 | } 117 | c := []DependentService{ 118 | DependentService{ 119 | Name: "abcd", 120 | EgressPort: 9003, 121 | }, 122 | DependentService{ 123 | Name: "abc", 124 | EgressPort: 9001, 125 | }, 126 | DependentService{ 127 | Name: "ab", 128 | EgressPort: 9002, 129 | }, 130 | } 131 | assert.True(t, EqualsServiceDependencies(a, sameA)) 132 | assert.False(t, EqualsServiceDependencies(a, b)) 133 | assert.False(t, EqualsServiceDependencies(a, c)) 134 | } 135 | -------------------------------------------------------------------------------- /src/model/version.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Version string 4 | -------------------------------------------------------------------------------- /src/repository/discovery_consul.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/consul/api" 7 | "github.com/pkg/errors" 8 | "github.com/rerorero/meshem/src/model" 9 | "github.com/rerorero/meshem/src/utils" 10 | ) 11 | 12 | type discoveryConsul struct { 13 | consul *utils.Consul 14 | globalName string 15 | } 16 | 17 | const ( 18 | // DefaultGlobalServiceName is default service name 19 | DefaultGlobalServiceName = "meshem_envoy" 20 | ) 21 | 22 | // NewDiscoveryConsul creates DiscoverRepository instance which uses a consul as datastore. 23 | func NewDiscoveryConsul(consul *utils.Consul, globalName string) DiscoveryRepository { 24 | gn := globalName 25 | if len(gn) == 0 { 26 | gn = DefaultGlobalServiceName 27 | } 28 | return &discoveryConsul{ 29 | consul: consul, 30 | globalName: gn, 31 | } 32 | } 33 | 34 | // Register registers an admin endpoint of host to the consul cagtalog 35 | func (dc *discoveryConsul) Register(host model.Host, tags map[string]string) error { 36 | adminAddr := host.GetAdminAddr() 37 | 38 | service := &api.AgentService{ 39 | Service: dc.globalName, 40 | Port: int(adminAddr.Port), 41 | } 42 | 43 | reg := &api.CatalogRegistration{ 44 | Node: host.Name, 45 | Address: adminAddr.Hostname, 46 | Datacenter: dc.consul.Datacenter, 47 | NodeMeta: tags, 48 | Service: service, 49 | } 50 | 51 | _, err := dc.consul.Client.Catalog().Register(reg, nil) 52 | if err != nil { 53 | return errors.Wrapf(err, "failed to register a catalog %+v", host) 54 | } 55 | return nil 56 | } 57 | 58 | // RegisterInfo finds a registered node by name. 59 | func (dc *discoveryConsul) FindByName(hostname string) (*DiscoveryInfo, bool, error) { 60 | node, _, err := dc.consul.Client.Catalog().Node(hostname, nil) 61 | if err != nil { 62 | return nil, false, errors.Wrapf(err, "failed to get a catalog %s", hostname) 63 | } 64 | if node == nil { 65 | return nil, false, nil 66 | } 67 | 68 | // make address 69 | addr := model.Address{Hostname: node.Node.Address} 70 | svc, ok := node.Services[dc.globalName] 71 | if ok { 72 | addr.Port = uint32(svc.Port) 73 | } 74 | 75 | if err != nil { 76 | return nil, false, fmt.Errorf("unexpected address format %s", node.Node.Address) 77 | } 78 | info := DiscoveryInfo{ 79 | Name: node.Node.Node, 80 | Address: addr, 81 | Tags: node.Node.Meta, 82 | } 83 | return &info, true, nil 84 | } 85 | 86 | // Unregister deregister host from datastore. 87 | func (dc *discoveryConsul) Unregister(hostname string) error { 88 | dereg := &api.CatalogDeregistration{ 89 | Node: hostname, 90 | } 91 | _, err := dc.consul.Client.Catalog().Deregister(dereg, nil) 92 | if err != nil { 93 | return errors.Wrapf(err, "failed to deregister host: %s", hostname) 94 | } 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /src/repository/discovery_consul_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/hashicorp/consul/api" 9 | "github.com/rerorero/meshem/src/model" 10 | "github.com/rerorero/meshem/src/utils" 11 | ) 12 | 13 | func unregisterAll(t *testing.T, consul *utils.Consul) { 14 | nodes, _, err := consul.Client.Catalog().Nodes(nil) 15 | assert.NoError(t, err) 16 | for _, n := range nodes { 17 | dereg := &api.CatalogDeregistration{ 18 | Node: n.Node, 19 | } 20 | _, err = consul.Client.Catalog().Deregister(dereg, nil) 21 | assert.NoError(t, err) 22 | } 23 | } 24 | 25 | func TestDiscoveryRegister(t *testing.T) { 26 | consul := utils.NewConsulMock() 27 | unregisterAll(t, consul) 28 | sut := NewDiscoveryConsul(consul, "") 29 | 30 | host, err := model.NewHost("reg1", "192.168.10.10:80", "127.0.0.1:8080", "127.0.0.1") 31 | assert.NoError(t, err) 32 | tags := map[string]string{} 33 | tags["aaa"] = "a3" 34 | tags["bbbb"] = "b4" 35 | 36 | err = sut.Register(host, tags) 37 | assert.NoError(t, err) 38 | 39 | info, ok, err := sut.FindByName("reg1") 40 | assert.NoError(t, err) 41 | assert.True(t, ok) 42 | assert.Equal(t, &DiscoveryInfo{ 43 | Name: host.Name, 44 | Address: *host.GetAdminAddr(), 45 | Tags: tags, 46 | }, info) 47 | 48 | // overwrite 49 | host2, err := model.NewHost("reg1", "192.168.20.30:9000", "127.0.0.1:9090", "127.0.0.1") 50 | tags["c"] = "c1" 51 | err = sut.Register(host2, tags) 52 | assert.NoError(t, err) 53 | 54 | info, ok, err = sut.FindByName("reg1") 55 | assert.NoError(t, err) 56 | assert.True(t, ok) 57 | assert.Equal(t, &DiscoveryInfo{ 58 | Name: host2.Name, 59 | Address: *host2.GetAdminAddr(), 60 | Tags: tags, 61 | }, info) 62 | 63 | // unregister 64 | err = sut.Unregister("reg1") 65 | assert.NoError(t, err) 66 | 67 | // not found 68 | info, ok, err = sut.FindByName("reg1") 69 | assert.NoError(t, err) 70 | assert.False(t, ok) 71 | 72 | // unregister unregistered 73 | err = sut.Unregister("reg1") 74 | assert.NoError(t, err) 75 | } 76 | -------------------------------------------------------------------------------- /src/repository/inventory_consul.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | // TODO: Use cas and lock to achieve stronger consistency 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/rerorero/meshem/src/model" 11 | "github.com/rerorero/meshem/src/utils" 12 | ) 13 | 14 | type inventoryConsul struct { 15 | consul *utils.Consul 16 | } 17 | 18 | const ( 19 | hostPrefix = "hosts" 20 | servicePrefix = "services" 21 | ) 22 | 23 | func NewInventoryConsul(consul *utils.Consul) InventoryRepository { 24 | return &inventoryConsul{consul: consul} 25 | } 26 | 27 | // Put Host object to Consul 28 | func (inventory *inventoryConsul) PutHost(host model.Host) error { 29 | js, err := json.Marshal(host) 30 | if err != nil { 31 | return errors.Wrapf(err, "Failed to marshal Host: %+v", host) 32 | } 33 | err = inventory.consul.PutKV(withHostPrefix(host.Name), string(js)) 34 | if err != nil { 35 | return err 36 | } 37 | return nil 38 | } 39 | 40 | func (inventory *inventoryConsul) SelectHostByName(name string) (host model.Host, ok bool, err error) { 41 | js, ok, err := inventory.consul.GetKV(withHostPrefix(name)) 42 | if err != nil { 43 | return host, false, err 44 | } 45 | if !ok { 46 | return host, false, nil 47 | } 48 | 49 | err = json.Unmarshal([]byte(js), &host) 50 | if err != nil { 51 | return host, false, errors.Wrapf(err, "Host object may be broken: %s", js) 52 | } 53 | 54 | return host, true, nil 55 | } 56 | 57 | // returns (true, nil) if it is deleted 58 | func (inventory *inventoryConsul) DeleteHost(name string) (bool, error) { 59 | return inventory.consul.DeleteTreeIfExists(withHostPrefix(name)) 60 | } 61 | 62 | func (inventory *inventoryConsul) SelectAllHostNames() ([]string, error) { 63 | names, err := inventory.consul.GetSubKeyNames(hostPrefix) 64 | if err != nil { 65 | return nil, errors.Wrap(err, "Failed to list host names") 66 | } 67 | return names, nil 68 | } 69 | 70 | func (inventory *inventoryConsul) SelectAllHosts() (hosts []model.Host, err error) { 71 | names, err := inventory.SelectAllHostNames() 72 | if err != nil { 73 | return nil, err 74 | } 75 | for _, name := range names { 76 | host, ok, err := inventory.SelectHostByName(name) 77 | if err != nil { 78 | return nil, err 79 | } 80 | if ok { 81 | hosts = append(hosts, host) 82 | } 83 | } 84 | return hosts, nil 85 | } 86 | 87 | func (inventory *inventoryConsul) SelectHostsOfService(service string) (hosts []model.Host, err error) { 88 | svc, ok, err := inventory.SelectServiceByName(service) 89 | if err != nil { 90 | return nil, err 91 | } 92 | if !ok { 93 | return nil, fmt.Errorf("No such service: %s", service) 94 | } 95 | 96 | for _, hostname := range svc.HostNames { 97 | host, ok, err := inventory.SelectHostByName(hostname) 98 | if err != nil { 99 | return nil, err 100 | } 101 | if ok { 102 | hosts = append(hosts, host) 103 | } 104 | } 105 | 106 | return hosts, nil 107 | } 108 | 109 | // Put Service object to Consul 110 | func (inventory *inventoryConsul) PutService(svc model.Service, version model.Version) error { 111 | svc.Version = version 112 | js, err := json.Marshal(svc) 113 | if err != nil { 114 | return errors.Wrapf(err, "Failed to marshal Service: %+v", svc) 115 | } 116 | err = inventory.consul.PutKV(withServicePrefix(svc.Name), string(js)) 117 | if err != nil { 118 | return err 119 | } 120 | return nil 121 | } 122 | 123 | func (inventory *inventoryConsul) SelectServiceByName(name string) (service model.Service, ok bool, err error) { 124 | js, ok, err := inventory.consul.GetKV(withServicePrefix(name)) 125 | if err != nil { 126 | return service, false, err 127 | } 128 | if !ok { 129 | return service, false, nil 130 | } 131 | 132 | err = json.Unmarshal([]byte(js), &service) 133 | if err != nil { 134 | return service, false, errors.Wrapf(err, "Service object may be broken: %s", js) 135 | } 136 | 137 | return service, true, nil 138 | } 139 | 140 | // returns (true, nil) if it is deleted 141 | func (inventory *inventoryConsul) DeleteService(name string) (bool, error) { 142 | return inventory.consul.DeleteTreeIfExists(withServicePrefix(name)) 143 | } 144 | 145 | func (inventory *inventoryConsul) SelectAllServiceNames() ([]string, error) { 146 | names, err := inventory.consul.GetSubKeyNames(servicePrefix) 147 | if err != nil { 148 | return nil, errors.Wrap(err, "Failed to list service names") 149 | } 150 | return names, nil 151 | } 152 | 153 | func (inventory *inventoryConsul) SelectAllServices() (services []model.Service, err error) { 154 | names, err := inventory.SelectAllServiceNames() 155 | if err != nil { 156 | return nil, err 157 | } 158 | for _, name := range names { 159 | service, ok, err := inventory.SelectServiceByName(name) 160 | if err != nil { 161 | return nil, err 162 | } 163 | if ok { 164 | services = append(services, service) 165 | } 166 | } 167 | return services, nil 168 | } 169 | 170 | // AddServiceDependency appends a service dependencey. 171 | func (inventory *inventoryConsul) AddServiceDependency(serviceName string, dependent model.DependentService, version model.Version) error { 172 | svc, ok, err := inventory.SelectServiceByName(serviceName) 173 | if err != nil { 174 | return err 175 | } 176 | if !ok { 177 | return fmt.Errorf("No such service: %s", serviceName) 178 | } 179 | 180 | // update dependency list 181 | err = svc.AppendDependent(dependent) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | return inventory.PutService(svc, version) 187 | } 188 | 189 | // RemoveServiceDependency removes a service depndency from service. 190 | func (inventory *inventoryConsul) RemoveServiceDependency(serviceName string, depend string, version model.Version) (bool, error) { 191 | svc, ok, err := inventory.SelectServiceByName(serviceName) 192 | if err != nil { 193 | return false, err 194 | } 195 | if !ok { 196 | return false, nil 197 | } 198 | 199 | // update dependency list 200 | removed := svc.RemoveDependent(depend) 201 | return removed, inventory.PutService(svc, version) 202 | } 203 | 204 | // SelectReferringServiceNamesTo taks names of all services which dependes on the service. 205 | func (inventory *inventoryConsul) SelectReferringServiceNamesTo(service string) (referrers []string, err error) { 206 | all, err := inventory.SelectAllServices() 207 | if err != nil { 208 | return nil, err 209 | } 210 | var i int 211 | for i = 0; i < len(all); i++ { 212 | ok, _ := all[i].FindDependentServiceName(service) 213 | if ok { 214 | referrers = append(referrers, all[i].Name) 215 | } 216 | } 217 | return referrers, nil 218 | } 219 | 220 | // returns an error if key doesn't exist 221 | func (inventory *inventoryConsul) getKVAddressExactly(key string) (*model.Address, error) { 222 | str, err := inventory.consul.GetKVExactly(key) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | addr, err := model.ParseAddress(str) 228 | if err != nil { 229 | return nil, errors.Wrapf(err, "%s=%s is invalid address", key, str) 230 | } 231 | return addr, nil 232 | } 233 | 234 | func (inventory *inventoryConsul) putKVAddress(key string, addr *model.Address) error { 235 | return inventory.consul.PutKV(key, addr.String()) 236 | } 237 | 238 | func withServicePrefix(sub string) string { 239 | return fmt.Sprintf("%s/%s", servicePrefix, sub) 240 | } 241 | 242 | func withHostPrefix(sub string) string { 243 | return fmt.Sprintf("%s/%s", hostPrefix, sub) 244 | } 245 | -------------------------------------------------------------------------------- /src/repository/inventory_heap.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rerorero/meshem/src/model" 7 | ) 8 | 9 | type inventoryHeap struct { 10 | services []*model.Service 11 | hosts []*model.Host 12 | } 13 | 14 | // NewInventoryHeap creates a heap inventory instance. 15 | func NewInventoryHeap() InventoryRepository { 16 | return &inventoryHeap{} 17 | } 18 | 19 | func (inv *inventoryHeap) PutHost(host model.Host) error { 20 | filtered := model.FilterHosts(inv.hosts, func(h *model.Host) bool { return h.Name == host.Name }) 21 | if len(filtered) == 1 { 22 | *filtered[0] = host 23 | } else if len(filtered) == 0 { 24 | inv.hosts = append(inv.hosts, &host) 25 | } else { 26 | return fmt.Errorf("duplicate hosts in the heap inventory: %s", host.Name) 27 | } 28 | return nil 29 | } 30 | 31 | func (inv *inventoryHeap) SelectHostByName(name string) (host model.Host, ok bool, err error) { 32 | filtered := model.FilterHosts(inv.hosts, func(h *model.Host) bool { return h.Name == name }) 33 | if len(filtered) == 1 { 34 | host = *filtered[0] 35 | return host, true, nil 36 | } else if len(filtered) == 0 { 37 | return host, false, nil 38 | } 39 | return host, false, fmt.Errorf("duplicate hosts in the heap inventory: %s", name) 40 | } 41 | 42 | func (inv *inventoryHeap) DeleteHost(name string) (bool, error) { 43 | filtered := model.FilterHosts(inv.hosts, func(h *model.Host) bool { return h.Name == name }) 44 | if len(filtered) == 1 { 45 | after := []*model.Host{} 46 | for _, h := range inv.hosts { 47 | if filtered[0] != h { 48 | after = append(after, h) 49 | } 50 | } 51 | inv.hosts = after 52 | return true, nil 53 | } else if len(filtered) == 0 { 54 | return false, nil 55 | } 56 | return false, fmt.Errorf("duplicate hosts in the heap inventory: %s", name) 57 | } 58 | 59 | func (inv *inventoryHeap) SelectAllHostNames() ([]string, error) { 60 | return model.MapHostsToString(inv.hosts, func(h *model.Host) string { return h.Name }), nil 61 | } 62 | 63 | func (inv *inventoryHeap) SelectAllHosts() (hosts []model.Host, err error) { 64 | for _, p := range inv.hosts { 65 | hosts = append(hosts, *p) 66 | } 67 | return hosts, nil 68 | } 69 | 70 | func (inv *inventoryHeap) SelectHostsOfService(service string) (hosts []model.Host, err error) { 71 | svc, ok, err := inv.SelectServiceByName(service) 72 | if err != nil { 73 | return nil, err 74 | } 75 | if !ok { 76 | return nil, fmt.Errorf("No such service: %s", service) 77 | } 78 | 79 | for _, hostname := range svc.HostNames { 80 | host, ok, err := inv.SelectHostByName(hostname) 81 | if err != nil { 82 | return nil, err 83 | } 84 | if ok { 85 | hosts = append(hosts, host) 86 | } 87 | } 88 | 89 | return hosts, nil 90 | } 91 | 92 | func (inv *inventoryHeap) PutService(svc model.Service, version model.Version) error { 93 | svc.Version = version 94 | filtered := model.FilterServices(inv.services, func(h *model.Service) bool { return h.Name == svc.Name }) 95 | if len(filtered) == 1 { 96 | *filtered[0] = svc 97 | } else if len(filtered) == 0 { 98 | inv.services = append(inv.services, &svc) 99 | } else { 100 | return fmt.Errorf("duplicate services in the heap inventory: %s", svc.Name) 101 | } 102 | return nil 103 | } 104 | 105 | func (inv *inventoryHeap) SelectServiceByName(name string) (model.Service, bool, error) { 106 | filtered := model.FilterServices(inv.services, func(h *model.Service) bool { return h.Name == name }) 107 | if len(filtered) == 1 { 108 | return *filtered[0], true, nil 109 | } else if len(filtered) == 0 { 110 | return model.Service{}, false, nil 111 | } 112 | return model.Service{}, false, fmt.Errorf("duplicate services in the heap inventory: %s", name) 113 | } 114 | 115 | func (inv *inventoryHeap) DeleteService(name string) (bool, error) { 116 | filtered := model.FilterServices(inv.services, func(h *model.Service) bool { return h.Name == name }) 117 | if len(filtered) == 1 { 118 | after := []*model.Service{} 119 | for _, h := range inv.services { 120 | if filtered[0] != h { 121 | after = append(after, h) 122 | } 123 | } 124 | inv.services = after 125 | return true, nil 126 | } else if len(filtered) == 0 { 127 | return false, nil 128 | } 129 | return false, fmt.Errorf("duplicate services in the heap inventory: %s", name) 130 | } 131 | 132 | func (inv *inventoryHeap) SelectAllServiceNames() ([]string, error) { 133 | return model.MapServicesToString(inv.services, func(h *model.Service) string { return h.Name }), nil 134 | } 135 | 136 | func (inv *inventoryHeap) SelectAllServices() (services []model.Service, err error) { 137 | for _, p := range inv.services { 138 | services = append(services, *p) 139 | } 140 | return services, nil 141 | } 142 | 143 | func (inv *inventoryHeap) AddServiceDependency(serviceName string, depend model.DependentService, version model.Version) error { 144 | svc, ok, err := inv.SelectServiceByName(serviceName) 145 | if err != nil { 146 | return err 147 | } 148 | if !ok { 149 | return fmt.Errorf("No such service: %s", serviceName) 150 | } 151 | 152 | // update dependency list 153 | err = svc.AppendDependent(depend) 154 | if err != nil { 155 | return err 156 | } 157 | return inv.PutService(svc, version) 158 | } 159 | 160 | // SelectReferringServiceNamesTo taks names of all services which dependes on the service. 161 | func (inv *inventoryHeap) SelectReferringServiceNamesTo(service string) (referrers []string, err error) { 162 | for _, svc := range inv.services { 163 | ok, _ := svc.FindDependentServiceName(service) 164 | if ok { 165 | referrers = append(referrers, svc.Name) 166 | } 167 | } 168 | return referrers, nil 169 | } 170 | 171 | // RemoveServiceDependency removes a service dependency from the service. 172 | func (inv *inventoryHeap) RemoveServiceDependency(serviceName string, depend string, version model.Version) (bool, error) { 173 | svc, ok, err := inv.SelectServiceByName(serviceName) 174 | if err != nil { 175 | return false, err 176 | } 177 | if !ok { 178 | return false, nil 179 | } 180 | 181 | // update dependency list 182 | removed := svc.RemoveDependent(depend) 183 | return removed, inv.PutService(svc, version) 184 | } 185 | -------------------------------------------------------------------------------- /src/repository/inventory_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/rerorero/meshem/src/model" 7 | "github.com/rerorero/meshem/src/utils" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGetServiceConsul(t *testing.T) { 12 | consul := utils.NewConsulMock() 13 | consul.Client.KV().DeleteTree(servicePrefix, nil) 14 | testGetService(t, NewInventoryConsul(consul)) 15 | } 16 | 17 | func TestGetServiceHeap(t *testing.T) { 18 | testGetService(t, NewInventoryHeap()) 19 | } 20 | 21 | func testGetService(t *testing.T, sut InventoryRepository) { 22 | dep1 := []model.DependentService{ 23 | model.DependentService{ 24 | Name: "svc1", 25 | EgressPort: 9001, 26 | }, 27 | model.DependentService{ 28 | Name: "svc2", 29 | EgressPort: 9002, 30 | }, 31 | } 32 | s1 := &model.Service{ 33 | Name: "service1", 34 | HostNames: []string{"host1", "host2"}, 35 | DependentServices: dep1, 36 | Version: "abc", 37 | } 38 | dep2 := []model.DependentService{ 39 | model.DependentService{ 40 | Name: "svc3", 41 | EgressPort: 9003, 42 | }, 43 | model.DependentService{ 44 | Name: "svc4", 45 | EgressPort: 9004, 46 | }, 47 | } 48 | s2 := &model.Service{ 49 | Name: "service2", 50 | HostNames: []string{"host3", "host4"}, 51 | DependentServices: dep2, 52 | Version: "def", 53 | } 54 | all := []model.Service{*s1, *s2} 55 | 56 | // put 57 | var err error 58 | for _, svc := range all { 59 | err = sut.PutService(svc, svc.Version) 60 | assert.NoError(t, err) 61 | } 62 | 63 | // by name 64 | for _, svc := range all { 65 | actual, ok, err := sut.SelectServiceByName(svc.Name) 66 | assert.NoError(t, err) 67 | assert.True(t, ok) 68 | assert.Equal(t, svc, actual) 69 | } 70 | // by name (not found) 71 | _, ok, err := sut.SelectServiceByName("unknown") 72 | assert.NoError(t, err) 73 | assert.False(t, ok) 74 | 75 | // select all names 76 | names, err := sut.SelectAllServiceNames() 77 | assert.NoError(t, err) 78 | assert.ElementsMatch(t, []string{s1.Name, s2.Name}, names) 79 | 80 | // select all 81 | services, err := sut.SelectAllServices() 82 | assert.NoError(t, err) 83 | assert.ElementsMatch(t, all, services) 84 | 85 | // delete one 86 | ok, err = sut.DeleteService(s1.Name) 87 | assert.NoError(t, err) 88 | assert.True(t, ok) 89 | ok, err = sut.DeleteService(s1.Name) 90 | assert.NoError(t, err) 91 | assert.False(t, ok) // already deleted 92 | // not found 93 | _, ok, err = sut.SelectServiceByName(s1.Name) 94 | assert.NoError(t, err) 95 | assert.False(t, ok) 96 | services, err = sut.SelectAllServices() 97 | assert.NoError(t, err) 98 | assert.Equal(t, []model.Service{*s2}, services) 99 | } 100 | 101 | func TestHostServiceConsul(t *testing.T) { 102 | consul := utils.NewConsulMock() 103 | consul.Client.KV().DeleteTree(hostPrefix, nil) 104 | testHostService(t, NewInventoryConsul(consul)) 105 | } 106 | 107 | func TestHostServiceHeap(t *testing.T) { 108 | testHostService(t, NewInventoryHeap()) 109 | } 110 | 111 | func testHostService(t *testing.T, sut InventoryRepository) { 112 | ip1, _ := model.ParseAddress("1.2.3.4:80") 113 | ip2, _ := model.ParseAddress("5.6.7.8:8080") 114 | ip3, _ := model.ParseAddress("9.0.1.2:80") 115 | ip4, _ := model.ParseAddress("3.4.5.6:8080") 116 | 117 | h1 := &model.Host{ 118 | Name: "host01", 119 | IngressAddr: *ip1, 120 | EgressHost: "127.0.0.1", 121 | SubstanceAddr: *ip2, 122 | } 123 | h2 := &model.Host{ 124 | Name: "host02", 125 | IngressAddr: *ip3, 126 | EgressHost: "127.0.0.1", 127 | SubstanceAddr: *ip4, 128 | } 129 | all := []model.Host{*h1, *h2} 130 | 131 | // put 132 | var err error 133 | for _, host := range all { 134 | err = sut.PutHost(host) 135 | assert.NoError(t, err) 136 | } 137 | 138 | // by name 139 | for _, host := range all { 140 | actual, ok, err := sut.SelectHostByName(host.Name) 141 | assert.NoError(t, err) 142 | assert.True(t, ok) 143 | assert.Equal(t, host, actual) 144 | } 145 | // by name (not found) 146 | _, ok, err := sut.SelectHostByName("unknown") 147 | assert.NoError(t, err) 148 | assert.False(t, ok) 149 | 150 | // select all names 151 | names, err := sut.SelectAllHostNames() 152 | assert.NoError(t, err) 153 | assert.ElementsMatch(t, []string{h1.Name, h2.Name}, names) 154 | 155 | // select all 156 | hosts, err := sut.SelectAllHosts() 157 | assert.NoError(t, err) 158 | assert.ElementsMatch(t, all, hosts) 159 | 160 | // delete one 161 | ok, err = sut.DeleteHost(h1.Name) 162 | assert.NoError(t, err) 163 | assert.True(t, ok) 164 | ok, err = sut.DeleteHost(h1.Name) 165 | assert.NoError(t, err) 166 | assert.False(t, ok) // already deleted 167 | // not found 168 | _, ok, err = sut.SelectHostByName(h1.Name) 169 | assert.NoError(t, err) 170 | assert.False(t, ok) 171 | hosts, err = sut.SelectAllHosts() 172 | assert.NoError(t, err) 173 | assert.Equal(t, []model.Host{*h2}, hosts) 174 | } 175 | 176 | func TestServiceDependenciesConsul(t *testing.T) { 177 | consul := utils.NewConsulMock() 178 | consul.Client.KV().DeleteTree(servicePrefix, nil) 179 | testServiceDependencies(t, NewInventoryConsul(consul)) 180 | } 181 | 182 | func TestServiceDependenciesHeap(t *testing.T) { 183 | testServiceDependencies(t, NewInventoryHeap()) 184 | } 185 | 186 | func testServiceDependencies(t *testing.T, sut InventoryRepository) { 187 | svcC := model.Service{ 188 | Name: "serviceC", 189 | Version: "abc", 190 | } 191 | 192 | svcB := model.Service{ 193 | Name: "serviceB", 194 | Version: "def", 195 | } 196 | 197 | svcA := model.Service{ 198 | Name: "serviceA", 199 | Version: "ghi", 200 | } 201 | allsvc := []*model.Service{&svcA, &svcB, &svcC} 202 | 203 | depB := []model.DependentService{ 204 | model.DependentService{ 205 | Name: svcC.Name, 206 | EgressPort: 9001, 207 | }, 208 | } 209 | depA := []model.DependentService{ 210 | model.DependentService{ 211 | Name: svcC.Name, 212 | EgressPort: 9001, 213 | }, 214 | model.DependentService{ 215 | Name: svcB.Name, 216 | EgressPort: 9002, 217 | }, 218 | } 219 | 220 | // register without dependencies 221 | var err error 222 | for _, svc := range allsvc { 223 | err = sut.PutService(*svc, svc.Version) 224 | assert.NoError(t, err) 225 | } 226 | 227 | // add dependencies 228 | newVersion := model.Version("xxxx") 229 | for _, dep := range depA { 230 | err = sut.AddServiceDependency(svcA.Name, dep, newVersion) 231 | assert.NoError(t, err) 232 | } 233 | for _, dep := range depB { 234 | err = sut.AddServiceDependency(svcB.Name, dep, newVersion) 235 | assert.NoError(t, err) 236 | } 237 | // version and dependencies are updated. 238 | svcA.Version = newVersion 239 | svcA.DependentServices = depA 240 | svcB.Version = newVersion 241 | svcB.DependentServices = depB 242 | 243 | // verify dependencies 244 | for _, svc := range allsvc { 245 | actual, ok, err := sut.SelectServiceByName(svc.Name) 246 | assert.NoError(t, err) 247 | assert.True(t, ok) 248 | assert.Equal(t, *svc, actual) 249 | } 250 | dependentA, err := sut.SelectReferringServiceNamesTo(svcA.Name) 251 | assert.NoError(t, err) 252 | assert.Empty(t, dependentA) 253 | dependentB, err := sut.SelectReferringServiceNamesTo(svcB.Name) 254 | assert.NoError(t, err) 255 | assert.ElementsMatch(t, []string{svcA.Name}, dependentB) 256 | dependentC, err := sut.SelectReferringServiceNamesTo(svcC.Name) 257 | assert.NoError(t, err) 258 | assert.ElementsMatch(t, []string{svcA.Name, svcB.Name}, dependentC) 259 | 260 | // remove dependencies 261 | newVersion = model.Version("yyyy") 262 | ok, err := sut.RemoveServiceDependency(svcA.Name, svcB.Name, newVersion) 263 | assert.NoError(t, err) 264 | assert.True(t, ok) 265 | actual, ok, err := sut.SelectServiceByName(svcA.Name) 266 | assert.NoError(t, err) 267 | assert.True(t, ok) 268 | assert.Equal(t, []model.DependentService{depA[0]}, actual.DependentServices) 269 | 270 | // error cases 271 | ok, err = sut.RemoveServiceDependency("unknown", svcA.Name, "zzzz") 272 | assert.NoError(t, err) 273 | assert.False(t, ok) 274 | } 275 | -------------------------------------------------------------------------------- /src/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import "github.com/rerorero/meshem/src/model" 4 | 5 | // InventoryRepository provides interface to control storage which stores the inventories information. 6 | type InventoryRepository interface { 7 | PutHost(host model.Host) error 8 | SelectHostByName(name string) (model.Host, bool, error) 9 | DeleteHost(name string) (bool, error) 10 | SelectAllHostNames() ([]string, error) 11 | SelectAllHosts() ([]model.Host, error) 12 | SelectHostsOfService(service string) ([]model.Host, error) 13 | PutService(svc model.Service, version model.Version) error 14 | SelectServiceByName(name string) (model.Service, bool, error) 15 | DeleteService(name string) (bool, error) 16 | SelectAllServiceNames() ([]string, error) 17 | SelectAllServices() ([]model.Service, error) 18 | AddServiceDependency(serviceName string, depend model.DependentService, version model.Version) error 19 | RemoveServiceDependency(serviceName string, depend string, version model.Version) (bool, error) 20 | SelectReferringServiceNamesTo(service string) ([]string, error) 21 | } 22 | 23 | type DiscoveryInfo struct { 24 | Name string 25 | Address model.Address 26 | Tags map[string]string 27 | } 28 | 29 | // DiscoveryRepository provides functions for discovery service registration 30 | type DiscoveryRepository interface { 31 | Register(host model.Host, tags map[string]string) error 32 | Unregister(hostname string) error 33 | FindByName(hostname string) (*DiscoveryInfo, bool, error) 34 | } 35 | -------------------------------------------------------------------------------- /src/start-mock-consul.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker run -d --rm --name=${CONSUL_MOCK_NAME:-consul_mock} \ 4 | -p ${CONSUL_PORT:-18500}:8500 \ 5 | -e CONSUL_BIND_INTERFACE=eth0 \ 6 | -e 'CONSUL_LOCAL_CONFIG={"acl_datacenter": "dc1", "acl_default_policy": "deny", "acl_master_token": "master"}' \ 7 | consul:${CONSUL_VERSION:-1.0.3} 8 | -------------------------------------------------------------------------------- /src/stop-mock-consul.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | container=${CONSUL_MOCK_NAME:-consul_mock} 3 | 4 | docker ps | grep "$container" && docker stop "$container" 5 | 6 | -------------------------------------------------------------------------------- /src/utils/consul.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/hashicorp/consul/api" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type Consul struct { 14 | Client *api.Client 15 | Datacenter string 16 | } 17 | 18 | func NewConsul(url *url.URL, token string, datacenter string) (*Consul, error) { 19 | config := api.DefaultConfig() 20 | config.Address = url.String() 21 | config.Token = token 22 | config.Datacenter = datacenter 23 | client, err := api.NewClient(config) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &Consul{ 28 | Client: client, 29 | Datacenter: datacenter, 30 | }, nil 31 | } 32 | 33 | func (c *Consul) GetKV(key string) (value string, ok bool, err error) { 34 | pair, _, err := c.Client.KV().Get(key, nil) 35 | if err != nil { 36 | return "", false, err 37 | } 38 | if pair == nil { 39 | return "", false, nil 40 | } 41 | return string(pair.Value), true, nil 42 | } 43 | 44 | // returns an error if key doesn't exist 45 | func (c *Consul) GetKVExactly(key string) (value string, err error) { 46 | value, ok, err := c.GetKV(key) 47 | if err != nil { 48 | return "", err 49 | } 50 | if !ok { 51 | return "", fmt.Errorf("key=%s does not exist", key) 52 | } 53 | return value, nil 54 | } 55 | 56 | func (c *Consul) PutKV(key string, value string) error { 57 | pair := &api.KVPair{Key: key, Value: []byte(value)} 58 | _, err := c.Client.KV().Put(pair, nil) 59 | if err != nil { 60 | return err 61 | } 62 | return nil 63 | } 64 | 65 | func (c *Consul) GetKeys(prefix string, recurse bool) ([]string, error) { 66 | if !strings.HasSuffix(prefix, "/") { 67 | prefix = prefix + "/" 68 | } 69 | keys, _, err := c.Client.KV().Keys(prefix, "", nil) 70 | if err != nil { 71 | return nil, err 72 | } 73 | if recurse { 74 | return keys, nil 75 | } 76 | 77 | // remove sub trees if not recusive 78 | keymap := make(map[string]struct{}) 79 | for _, key := range keys { 80 | sub := strings.Split(key[len(prefix):], "/")[0] 81 | if sub != "" { 82 | keymap[sub] = struct{}{} 83 | } 84 | } 85 | 86 | replacedKeys := []string{} 87 | for key := range keymap { 88 | replacedKeys = append(replacedKeys, prefix+key) 89 | } 90 | 91 | return replacedKeys, nil 92 | } 93 | 94 | // get keys which is first children of the prefix and removes the prefix 95 | func (c *Consul) GetSubKeyNames(prefix string) ([]string, error) { 96 | if !strings.HasSuffix(prefix, "/") { 97 | prefix = prefix + "/" 98 | } 99 | fullkeys, err := c.GetKeys(prefix, false) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | keys := []string{} 105 | prelen := len(prefix) 106 | for _, key := range fullkeys { 107 | keys = append(keys, string(key[prelen:])) 108 | } 109 | return keys, nil 110 | } 111 | 112 | // returns an error if key doesn't exist 113 | func (c *Consul) GetKVBoolExactly(key string) (value bool, err error) { 114 | str, err := c.GetKVExactly(key) 115 | if err != nil { 116 | return false, err 117 | } 118 | if str == "true" { 119 | return true, nil 120 | } else if str == "false" { 121 | return false, nil 122 | } 123 | return false, fmt.Errorf("%s=%s is not boolean", key, str) 124 | } 125 | 126 | func (c *Consul) PutKVBool(key string, value bool) error { 127 | str := "" 128 | if value { 129 | str = "true" 130 | } else { 131 | str = "false" 132 | } 133 | return c.PutKV(key, str) 134 | } 135 | 136 | // returns an error if key doesn't exist 137 | func (c *Consul) GetKVIntExactly(key string) (value int, err error) { 138 | str, err := c.GetKVExactly(key) 139 | if err != nil { 140 | return 0, err 141 | } 142 | 143 | num, err := strconv.Atoi(str) 144 | if err != nil { 145 | return 0, errors.Wrapf(err, "%s=%s is not int", key, str) 146 | } 147 | return num, nil 148 | } 149 | 150 | func (c *Consul) PutKVInt(key string, num int) error { 151 | return c.PutKV(key, strconv.Itoa(num)) 152 | } 153 | 154 | // returns whether or not to delete 155 | func (c *Consul) DeleteTreeIfExists(key string) (bool, error) { 156 | _, ok, err := c.GetKV(key) 157 | if err != nil { 158 | return false, err 159 | } 160 | if !ok { 161 | return false, nil 162 | } 163 | 164 | _, err = c.Client.KV().DeleteTree(key, nil) 165 | if err != nil { 166 | return false, err 167 | } 168 | 169 | return true, nil 170 | } 171 | 172 | // only for test 173 | func NewConsulMock() *Consul { 174 | // It must be set the same as start-mock-consul.sh 175 | addr, err := url.Parse("http://127.0.0.1:18500") 176 | if err != nil { 177 | panic(err) 178 | } 179 | consul, err := NewConsul(addr, "master", "dc1") 180 | if err != nil { 181 | panic(err) 182 | } 183 | 184 | _, err = consul.Client.Agent().NodeName() 185 | if err != nil { 186 | if strings.Contains(err.Error(), "connection refused") { 187 | panic("It seems that the mock consul is not running. You need to execute start-mock-consul.sh.") 188 | } 189 | panic(err.Error()) 190 | } 191 | return consul 192 | } 193 | -------------------------------------------------------------------------------- /src/utils/consul_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPutGetKV(t *testing.T) { 11 | mock := NewConsulMock() 12 | 13 | // delete 14 | mock.Client.KV().DeleteTree("_testing", nil) 15 | 16 | err := mock.PutKV("_testing/foo/bar", "abc") 17 | assert.NoError(t, err) 18 | 19 | actual, ok, err := mock.GetKV("_testing/foo/bar") 20 | assert.NoError(t, err) 21 | assert.Equal(t, actual, "abc") 22 | assert.True(t, ok) 23 | 24 | _, err = mock.Client.KV().Delete("_testing/foo/bar", nil) 25 | assert.NoError(t, err) 26 | 27 | actual, ok, err = mock.GetKV("_testing/foo/bar") 28 | assert.NoError(t, err) 29 | assert.False(t, ok) 30 | } 31 | 32 | func TestGetKeys(t *testing.T) { 33 | mock := NewConsulMock() 34 | 35 | // delete 36 | mock.Client.KV().DeleteTree("_testing", nil) 37 | 38 | err := mock.PutKV("_testing/a/b", "a/b") 39 | assert.NoError(t, err) 40 | err = mock.PutKV("_testing/a/bb", "a/bb") 41 | assert.NoError(t, err) 42 | err = mock.PutKV("_testing/a/b/c", "a/b/c") 43 | assert.NoError(t, err) 44 | err = mock.PutKV("_testing/a/bbb/c", "a/b/c") 45 | assert.NoError(t, err) 46 | err = mock.PutKV("_testing/aa/b", "aa/b") 47 | assert.NoError(t, err) 48 | 49 | // recursive 50 | var keys []string 51 | keys, err = mock.GetKeys("_testing/a", true) 52 | assert.NoError(t, err) 53 | expected := []string{"_testing/a/b", "_testing/a/bb", "_testing/a/b/c", "_testing/a/bbb/c"} 54 | sort.Strings(keys) 55 | sort.Strings(expected) 56 | assert.Equal(t, keys, expected) 57 | 58 | // not recursive 59 | keys, err = mock.GetKeys("_testing/a", false) 60 | assert.NoError(t, err) 61 | expected = []string{"_testing/a/b", "_testing/a/bb", "_testing/a/bbb"} 62 | sort.Strings(keys) 63 | sort.Strings(expected) 64 | assert.Equal(t, keys, expected) 65 | 66 | // Get first children nodes 67 | keys, err = mock.GetSubKeyNames("_testing/a") 68 | assert.NoError(t, err) 69 | expected = []string{"b", "bb", "bbb"} 70 | sort.Strings(keys) 71 | sort.Strings(expected) 72 | assert.Equal(t, keys, expected) 73 | 74 | // not found 75 | keys, err = mock.GetKeys("_testing/ab", true) 76 | assert.NoError(t, err) 77 | assert.Empty(t, keys) 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func ContainsString(slice []string, value string) (index int, ok bool) { 4 | for i, v := range slice { 5 | if value == v { 6 | return i, true 7 | } 8 | } 9 | return -1, false 10 | } 11 | 12 | // RemoveFromStringSlice removes a string entity from the string slice. 13 | func RemoveFromStringSlice(slice []string, value string) (removed []string) { 14 | for _, v := range slice { 15 | if v != value { 16 | removed = append(removed, v) 17 | } 18 | } 19 | return removed 20 | } 21 | 22 | // FilterNotContainsString removes all items in 'filterElements' from the 'slice'. 23 | func FilterNotContainsString(slice []string, filterElements []string) (filtered []string) { 24 | elements := map[string]struct{}{} 25 | var i int 26 | for i = 0; i < len(filterElements); i++ { 27 | elements[filterElements[i]] = struct{}{} 28 | } 29 | for _, s := range slice { 30 | _, ok := elements[s] 31 | if !ok { 32 | filtered = append(filtered, s) 33 | } 34 | } 35 | return filtered 36 | } 37 | 38 | // IntersectStringSlice returns intersections. 39 | func IntersectStringSlice(sliceA []string, sliceB []string) (intersect []string) { 40 | elements := map[string]struct{}{} 41 | var i int 42 | for i = 0; i < len(sliceB); i++ { 43 | elements[sliceB[i]] = struct{}{} 44 | } 45 | for _, s := range sliceA { 46 | _, ok := elements[s] 47 | if ok { 48 | intersect = append(intersect, s) 49 | } 50 | } 51 | return intersect 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/slice_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestContainsString(t *testing.T) { 10 | slice := []string{"a", "bb", "c", "dd", "e"} 11 | var i int 12 | for _, s := range slice { 13 | idx, ok := ContainsString(slice, s) 14 | assert.True(t, ok) 15 | assert.Equal(t, idx, i) 16 | i++ 17 | } 18 | _, ok := ContainsString(slice, "b") 19 | assert.False(t, ok) 20 | } 21 | 22 | func TestRemoveFromStringSlice(t *testing.T) { 23 | slice := []string{"a", "bb", "c", "dd", "e"} 24 | actual := RemoveFromStringSlice(slice, "bb") 25 | assert.ElementsMatch(t, []string{"a", "c", "dd", "e"}, actual) 26 | actual = RemoveFromStringSlice(slice, "b") 27 | assert.ElementsMatch(t, slice, actual) 28 | } 29 | 30 | func TestFilterNotContainsString(t *testing.T) { 31 | sa := []string{"a", "bb", "c", "dd", "e"} 32 | sb := []string{"aa", "bb", "cc", "dd", "ee"} 33 | assert.ElementsMatch(t, []string{"a", "c", "e"}, FilterNotContainsString(sa, sb)) 34 | assert.ElementsMatch(t, []string{"aa", "cc", "ee"}, FilterNotContainsString(sb, sa)) 35 | } 36 | 37 | func TestIntersectStringSlice(t *testing.T) { 38 | sa := []string{"a", "bb", "c", "dd", "e"} 39 | sb := []string{"aa", "bb", "cc", "dd", "ee"} 40 | assert.ElementsMatch(t, []string{"bb", "dd"}, IntersectStringSlice(sa, sb)) 41 | assert.ElementsMatch(t, []string{"bb", "dd"}, IntersectStringSlice(sb, sa)) 42 | } 43 | -------------------------------------------------------------------------------- /src/version.go: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | func MeshemVersion() string { 4 | return "0.1.1" 5 | } 6 | --------------------------------------------------------------------------------