├── .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 | [](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 |
--------------------------------------------------------------------------------