From e62a52f5a3f5bc5f56c88477fbb02931aeb07547 Mon Sep 17 00:00:00 2001 From: kanishkarj Date: Fri, 5 Jun 2020 19:02:20 +0530 Subject: [PATCH 01/12] Store build locally Signed-off-by: kanishkarj --- service/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/Dockerfile b/service/Dockerfile index d72b4b3..3e9e51c 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -1,5 +1,5 @@ FROM golang:1.13.5 as bd WORKDIR /github.com/layer5io/sample-app-service ADD . . -RUN GOPROXY=direct GOSUMDB=off go build -a -o /main . -CMD ["/main"] \ No newline at end of file +RUN GOPROXY=direct GOSUMDB=off go build -a -o ./main . +CMD ["./main"] \ No newline at end of file From c4d117531596660faa9434f258ef04288663f3dc Mon Sep 17 00:00:00 2001 From: kanishkarj Date: Tue, 9 Jun 2020 16:36:18 +0530 Subject: [PATCH 02/12] Send additional data with the requests Signed-off-by: kanishkarj --- Makefile | 9 +++- service/main.go | 113 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 89 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index 0acb958..e13a42c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,14 @@ build-service: cd service && go build -a -o ./main . -run-service: build-service +run-service-a: + SERVICE_NAME="service-a" \ + PORT=9091 \ + ./service/main + +run-service-b: + SERVICE_NAME="service-b" \ + PORT=9092 \ ./service/main build-img-service: diff --git a/service/main.go b/service/main.go index dfb6cae..9c6b4af 100644 --- a/service/main.go +++ b/service/main.go @@ -5,42 +5,49 @@ import ( "io/ioutil" "net/http" "os" - "strconv" "strings" "sync" logrus "github.com/sirupsen/logrus" ) -var requestsReceived int -var responsesSucceeded int -var responsesFailed int +var requestsReceived []string +var responsesSucceeded []string +var responsesFailed []string var mutex sync.Mutex -func exclusive(fn func()) { +func execExclusive(fn func()) { defer mutex.Unlock() mutex.Lock() fn() } +const serviceID = "ServiceName" + +var serviceName string + func MetricsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - exclusive(func() { requestsReceived++ }) + execExclusive(func() { + svcName := r.Header.Get(serviceID) + if svcName == "" { + svcName = "Unidentified" + } + requestsReceived = append(requestsReceived, svcName) + }) next.ServeHTTP(w, r) }) } -func call(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodPost { - http.Error(w, "Method not defined", http.StatusBadRequest) - logrus.Errorf("Method not defined") +func call(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { return } - defer req.Body.Close() + defer r.Body.Close() - var data map[string]string - bytes, err := ioutil.ReadAll(req.Body) + var data map[string]interface{} + bytes, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, "Error reading body", http.StatusBadRequest) logrus.Errorf("Error reading body: %s", err.Error()) @@ -59,18 +66,38 @@ func call(w http.ResponseWriter, req *http.Request) { return } - host := data["host"] - body := data["body"] + url := data["url"].(string) + method := data["method"].(string) + headers := data["headers"] + body := data["body"].(string) + + var req *http.Request + if method == http.MethodPost || method == http.MethodPatch || method == http.MethodPut { + req, err = http.NewRequest(method, url, strings.NewReader(body)) + if err != nil { + logrus.Errorf("Error creating request %s", err.Error()) + // TODO err handling + } + } else { + req, err = http.NewRequest(method, url, nil) + if err != nil { + logrus.Errorf("Error creating request %s", err.Error()) + } + } - if host == "" { - return + client := http.Client{} + + if headers != nil { + headers := headers.(map[string]interface{}) + for key, val := range headers { + req.Header.Add(key, val.(string)) + } + req.Header.Add(serviceID, serviceName) } - var resp *http.Response - if body != "" { - resp, err = http.Post(host, "application/json", strings.NewReader(body)) - } else { - resp, err = http.Get(host) + resp, err := client.Do(req) + if err != nil { + logrus.Errorf("Error completing the request %s", err.Error()) } logrus.Debugf("Call response: %v", resp) @@ -82,9 +109,13 @@ func call(w http.ResponseWriter, req *http.Request) { if resp.StatusCode >= 200 && resp.StatusCode < 400 { w.WriteHeader(http.StatusOK) - exclusive(func() { responsesSucceeded++ }) + execExclusive(func() { + responsesSucceeded = append(responsesSucceeded, url) + }) } else { - exclusive(func() { responsesFailed++ }) + execExclusive(func() { + responsesFailed = append(responsesFailed, url) + }) } bytes, err = ioutil.ReadAll(resp.Body) @@ -99,17 +130,19 @@ func call(w http.ResponseWriter, req *http.Request) { func metrics(w http.ResponseWriter, req *http.Request) { if req.Method == http.MethodGet { w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]string{ - "responsesSucceeded": strconv.Itoa(responsesSucceeded), - "responsesFailed": strconv.Itoa(responsesFailed), - "requestsReceived": strconv.Itoa(requestsReceived), + if err := json.NewEncoder(w).Encode(map[string][]string{ + "responsesSucceeded": responsesSucceeded, + "responsesFailed": responsesFailed, + "requestsReceived": requestsReceived, }); err != nil { w.WriteHeader(http.StatusInternalServerError) } } else if req.Method == http.MethodDelete { - responsesSucceeded = 0 - responsesFailed = 0 - requestsReceived = 0 + execExclusive(func() { + responsesSucceeded = []string{} + responsesFailed = []string{} + requestsReceived = []string{} + }) } else { http.Error(w, "Method not defined", http.StatusBadRequest) } @@ -118,8 +151,24 @@ func metrics(w http.ResponseWriter, req *http.Request) { func main() { logrus.SetOutput(os.Stdout) + serviceName = os.Getenv("SERVICE_NAME") + if serviceName == "" { + serviceName = "Default" + } + + port := os.Getenv("PORT") + if port == "" { + port = "9091" + } + + responsesSucceeded = []string{} + responsesFailed = []string{} + requestsReceived = []string{} + mux := http.NewServeMux() mux.Handle("/call", MetricsMiddleware(http.HandlerFunc(call))) mux.Handle("/metrics", http.HandlerFunc(metrics)) - http.ListenAndServe(":9091", mux) + logrus.Debugf("Started serving at: 9091") + println("Started serving at: " + port) + http.ListenAndServe(":"+port, mux) } From edec7f9db87eff131a75ff9a92613137e318f3c0 Mon Sep 17 00:00:00 2001 From: kanishkarj Date: Tue, 9 Jun 2020 17:35:08 +0530 Subject: [PATCH 03/12] Add echo route Signed-off-by: kanishkarj --- service/main.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/service/main.go b/service/main.go index 9c6b4af..f59824d 100644 --- a/service/main.go +++ b/service/main.go @@ -127,6 +127,10 @@ func call(w http.ResponseWriter, r *http.Request) { w.Write(bytes) } +func echo(w http.ResponseWriter, req *http.Request) { + req.Write(w) +} + func metrics(w http.ResponseWriter, req *http.Request) { if req.Method == http.MethodGet { w.Header().Set("Content-Type", "application/json") @@ -150,6 +154,7 @@ func metrics(w http.ResponseWriter, req *http.Request) { func main() { logrus.SetOutput(os.Stdout) + logrus.SetLevel(logrus.DebugLevel) serviceName = os.Getenv("SERVICE_NAME") if serviceName == "" { @@ -166,9 +171,10 @@ func main() { requestsReceived = []string{} mux := http.NewServeMux() + mux.Handle("/call", MetricsMiddleware(http.HandlerFunc(call))) mux.Handle("/metrics", http.HandlerFunc(metrics)) - logrus.Debugf("Started serving at: 9091") - println("Started serving at: " + port) + mux.Handle("/echo", http.HandlerFunc(echo)) + logrus.Infof("Started serving at: %s", port) http.ListenAndServe(":"+port, mux) } From 7694afed8962db8d510b5982e9429cec374ab1eb Mon Sep 17 00:00:00 2001 From: kanishkarj Date: Tue, 9 Jun 2020 20:41:07 +0530 Subject: [PATCH 04/12] More detailed metrics Signed-off-by: kanishkarj --- service/main.go | 85 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/service/main.go b/service/main.go index f59824d..90f9945 100644 --- a/service/main.go +++ b/service/main.go @@ -11,9 +11,20 @@ import ( logrus "github.com/sirupsen/logrus" ) -var requestsReceived []string -var responsesSucceeded []string -var responsesFailed []string +type MetricsResponse struct { + URL string + Method string + Headers map[string]interface{} +} + +type Metrics struct { + ReqReceived []string + RespSucceeded []MetricsResponse + RespFailed []MetricsResponse +} + +var metricsObj Metrics + var mutex sync.Mutex func execExclusive(fn func()) { @@ -23,6 +34,7 @@ func execExclusive(fn func()) { } const serviceID = "ServiceName" +const defaultRequestID = "Default" var serviceName string @@ -33,7 +45,7 @@ func MetricsMiddleware(next http.Handler) http.Handler { if svcName == "" { svcName = "Unidentified" } - requestsReceived = append(requestsReceived, svcName) + metricsObj.ReqReceived = append(metricsObj.ReqReceived, svcName) }) next.ServeHTTP(w, r) }) @@ -76,12 +88,15 @@ func call(w http.ResponseWriter, r *http.Request) { req, err = http.NewRequest(method, url, strings.NewReader(body)) if err != nil { logrus.Errorf("Error creating request %s", err.Error()) - // TODO err handling + http.Error(w, "Error creating request", http.StatusBadRequest) + return } } else { req, err = http.NewRequest(method, url, nil) if err != nil { logrus.Errorf("Error creating request %s", err.Error()) + http.Error(w, "Error creating request", http.StatusBadRequest) + return } } @@ -98,6 +113,8 @@ func call(w http.ResponseWriter, r *http.Request) { resp, err := client.Do(req) if err != nil { logrus.Errorf("Error completing the request %s", err.Error()) + http.Error(w, "Error completing the request", http.StatusInternalServerError) + return } logrus.Debugf("Call response: %v", resp) @@ -110,11 +127,35 @@ func call(w http.ResponseWriter, r *http.Request) { if resp.StatusCode >= 200 && resp.StatusCode < 400 { w.WriteHeader(http.StatusOK) execExclusive(func() { - responsesSucceeded = append(responsesSucceeded, url) + if headers != nil { + headers := headers.(map[string]interface{}) + metricsObj.RespSucceeded = append(metricsObj.RespSucceeded, MetricsResponse{ + URL: url, + Method: method, + Headers: headers, + }) + } else { + metricsObj.RespSucceeded = append(metricsObj.RespSucceeded, MetricsResponse{ + URL: url, + Method: method, + }) + } }) } else { execExclusive(func() { - responsesFailed = append(responsesFailed, url) + if headers != nil { + headers := headers.(map[string]interface{}) + metricsObj.RespFailed = append(metricsObj.RespFailed, MetricsResponse{ + URL: url, + Method: method, + Headers: headers, + }) + } else { + metricsObj.RespFailed = append(metricsObj.RespFailed, MetricsResponse{ + URL: url, + Method: method, + }) + } }) } @@ -134,18 +175,18 @@ func echo(w http.ResponseWriter, req *http.Request) { func metrics(w http.ResponseWriter, req *http.Request) { if req.Method == http.MethodGet { w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string][]string{ - "responsesSucceeded": responsesSucceeded, - "responsesFailed": responsesFailed, - "requestsReceived": requestsReceived, - }); err != nil { - w.WriteHeader(http.StatusInternalServerError) - } + execExclusive(func() { + if err := json.NewEncoder(w).Encode(&metricsObj); err != nil { + w.WriteHeader(http.StatusInternalServerError) + } + }) } else if req.Method == http.MethodDelete { execExclusive(func() { - responsesSucceeded = []string{} - responsesFailed = []string{} - requestsReceived = []string{} + metricsObj = Metrics{ + RespSucceeded: []MetricsResponse{}, + RespFailed: []MetricsResponse{}, + ReqReceived: []string{}, + } }) } else { http.Error(w, "Method not defined", http.StatusBadRequest) @@ -166,15 +207,17 @@ func main() { port = "9091" } - responsesSucceeded = []string{} - responsesFailed = []string{} - requestsReceived = []string{} + metricsObj = Metrics{ + RespSucceeded: []MetricsResponse{}, + RespFailed: []MetricsResponse{}, + ReqReceived: []string{}, + } mux := http.NewServeMux() mux.Handle("/call", MetricsMiddleware(http.HandlerFunc(call))) mux.Handle("/metrics", http.HandlerFunc(metrics)) - mux.Handle("/echo", http.HandlerFunc(echo)) + mux.Handle("/echo", MetricsMiddleware(http.HandlerFunc(echo))) logrus.Infof("Started serving at: %s", port) http.ListenAndServe(":"+port, mux) } From 8b36c0895af3f2156f236a3f0ceca376f0bad054 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Tue, 9 Jun 2020 11:18:24 -0500 Subject: [PATCH 05/12] Update README.md --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 636a6c8..2c43b96 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,23 @@ -



- - +

+

+![GitHub contributors](https://img.shields.io/github/contributors/layer5io/layer5.svg) +![GitHub](https://img.shields.io/github/license/layer5io/layer5.svg) [![Docker Pulls](https://img.shields.io/docker/pulls/layer5/learn-layer5.svg)](https://hub.docker.com/r/layer5/learn-layer5) [![Go Report Card](https://goreportcard.com/badge/github.com/layer5io/learn-layer5)](https://goreportcard.com/report/github.com/layer5io/learn-layer5) -[![GitHub issues by-label](https://img.shields.io/github/issues/layer5io/learn-layer5/help%20wanted.svg)](https://github.com/layer5io/learn-layer5/issues?q=is%3Aopen+is%3Aissue+label%3A"help+wanted") -[![Website](https://img.shields.io/website/https/layer5.io/meshery.svg)](https://layer5.io/meshery/) +[![GitHub issues by-label](https://img.shields.io/github/issues/layer5io/learn-layer5/help%20wanted.svg)](https://github.com/issues?utf8=βœ“&q=is%3Aopen+is%3Aissue+archived%3Afalse+org%3Alayer5io+label%3A%22help+wanted%22+") +[![Website](https://img.shields.io/website/https/layer5.io/meshery.svg)](https://layer5.io) [![Twitter Follow](https://img.shields.io/twitter/follow/layer5.svg?label=Follow&style=social)](https://twitter.com/intent/follow?screen_name=mesheryio) [![Slack](http://slack.layer5.io/badge.svg)](http://slack.layer5.io) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3564/badge)](https://bestpractices.coreinfrastructure.org/projects/3564) +

+

Learn Layer5

+The Learn Layer5 sample application is used as: -# learn-layer5 - +- a learning device +- for Service Mesh Interface conformance ## Service @@ -111,4 +115,36 @@ curl --location --request DELETE 'localhost:9091/metrics' \ "hello": "bye" }' # No Output -``` \ No newline at end of file +``` + +

If you’re using Learn Layer5 or if you like the project, please β˜… star this repository to show your support! 🀩

+

+ +

+

Community and Contributing

+Our projects are community-built and welcome collaboration. πŸ‘ Be sure to see the Meshery Contributors Welcome Guide for a tour of resources available to you and jump into our Slack! Contributors are expected to adhere to the CNCF Code of Conduct. + +Layer5 Service Mesh Community + +Layer5 Service Mesh Community + +

+βœ”οΈ Join weekly community meeting on Fridays from 10am - 11am Central.
+βœ”οΈ Watch community meeting recordings.
+βœ”οΈ Access the community drive.
+

+

+Not sure where to start? Grab an open issue with the help-wanted label. +

+ +## About Layer5 + +**Community First** +

The Layer5 community represents the largest collection of service mesh projects and their maintainers in the world.

+ +**Open Source First** +

We build projects to provide learning environments, deployment and operational best practices, performance benchmarks, create documentation, share networking opportunities, and more. Our shared commitment to the open source spirit pushes Layer5 projects forward.

+ +**License** + +This repository and site are available as open source under the terms of the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0). From 7f3f1e0bf96285e039612af80d2831b594d9b56a Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Tue, 9 Jun 2020 11:19:17 -0500 Subject: [PATCH 06/12] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c43b96..e4f0647 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,11 @@

Learn Layer5

+ The Learn Layer5 sample application is used as: - a learning device -- for Service Mesh Interface conformance +- for [Service Mesh Interface conformance](https://docs.google.com/document/d/1HL8Sk7NSLLj-9PRqoHYVIGyU6fZxUQFotrxbmfFtjwc/edit#) ## Service From 10b93fa89dc313cb3f014a82e8a0f056479e5e26 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Tue, 9 Jun 2020 11:19:53 -0500 Subject: [PATCH 07/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4f0647..bd678f6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The Learn Layer5 sample application is used as: -- a learning device +- a learning device (for [service mesh workshops](https://layer5.io/workshops) - for [Service Mesh Interface conformance](https://docs.google.com/document/d/1HL8Sk7NSLLj-9PRqoHYVIGyU6fZxUQFotrxbmfFtjwc/edit#) ## Service From bc62e88be631386b581bd45d2d4013d9cd8f8131 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Tue, 9 Jun 2020 11:22:31 -0500 Subject: [PATCH 08/12] Update README.md --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bd678f6..61bc5f2 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,17 @@ The Learn Layer5 sample application is used as: -- a learning device (for [service mesh workshops](https://layer5.io/workshops) +- a learning device (for [service mesh workshops](https://layer5.io/workshops)) - for [Service Mesh Interface conformance](https://docs.google.com/document/d/1HL8Sk7NSLLj-9PRqoHYVIGyU6fZxUQFotrxbmfFtjwc/edit#) -## Service +## Application Architecture +The Learn Layer5 application includes three services: `app-a`, `app-b`, and `app-c`. Each service is listening on port `9091/tcp`. + +### Service The following are the routes defined by the `service` app and their functionality. -##### POST /call +#### POST /call This is the route whose metrics will be collected by the app. This route can be used to make the service call any other web service. @@ -87,7 +90,7 @@ curl --location --request POST 'http://localhost:9091/call' \ } ``` -##### GET /metrics +#### GET /metrics Gets the metrics from `service` ```shell @@ -105,7 +108,7 @@ curl --location --request GET 'localhost:9091/metrics' \ } ``` -##### DELETE /metrics +#### DELETE /metrics Clears the counters in `service` ```shell From 8fafd3f024cf9a71b9e205bfc16da11ea14b2ce1 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Fri, 12 Jun 2020 11:01:27 -0500 Subject: [PATCH 09/12] proliferate this app across all adapters. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 61bc5f2..cc2a37b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@

Learn Layer5

-The Learn Layer5 sample application is used as: +The Learn Layer5 sample application is to be available for use across all service meshes that Meshery supports and is to used as: - a learning device (for [service mesh workshops](https://layer5.io/workshops)) - for [Service Mesh Interface conformance](https://docs.google.com/document/d/1HL8Sk7NSLLj-9PRqoHYVIGyU6fZxUQFotrxbmfFtjwc/edit#) From 1f5d88708472cb51e0476066d112cf6200b64b61 Mon Sep 17 00:00:00 2001 From: Kanishkar J Date: Mon, 15 Jun 2020 20:57:14 +0530 Subject: [PATCH 10/12] Update Makefile Signed-off-by: kanishkarj --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e13a42c..7a97123 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +VER=$(shell git rev-parse --short HEAD) + build-service: cd service && go build -a -o ./main . @@ -12,4 +14,7 @@ run-service-b: ./service/main build-img-service: - cd service && docker build -t layer5/sample-app-service:dev . + cd service && docker build -t layer5/learn-layer5:latest -t layer5/learn-layer5:$(VER) . + +image-push: + docker push layer5/learn-layer5 From 0fc3ef9e9bdaa3480eef802dd7a26badee3ca88f Mon Sep 17 00:00:00 2001 From: Kanishkar J Date: Mon, 15 Jun 2020 21:00:01 +0530 Subject: [PATCH 11/12] Create ci.yml Signed-off-by: kanishkarj --- .github/workflows/ci.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b79a4a4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: Learn Layer5 + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + docker: + name: Docker build and push + runs-on: ubuntu-latest + steps: + - name: Check out code + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master') && success() + uses: actions/checkout@master + with: + fetch-depth: 1 + - name: Docker login + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master') && success() + uses: azure/docker-login@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Docker build & push + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master') && success() + run: | + make build-img-service + make image-push From 24cf2d89f04574e1789edf38e63bcd40aa556398 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Mon, 15 Jun 2020 11:16:10 -0500 Subject: [PATCH 12/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc2a37b..c846121 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ curl --location --request DELETE 'localhost:9091/metrics' \

Community and Contributing

-Our projects are community-built and welcome collaboration. πŸ‘ Be sure to see the Meshery Contributors Welcome Guide for a tour of resources available to you and jump into our Slack! Contributors are expected to adhere to the CNCF Code of Conduct. +Our projects are community-built and welcome collaboration. πŸ‘ Be sure to see the Layer5 Contributor Welcome Guide for a tour of resources available to you and jump into our Slack! Contributors are expected to adhere to the CNCF Code of Conduct. Layer5 Service Mesh Community