Merge remote-tracking branch 'origin' into createCartService
This commit is contained in:
commit
11c208a9f4
18 changed files with 354 additions and 87 deletions
40
README.md
40
README.md
|
@ -1 +1,41 @@
|
||||||
# Microservices demo
|
# Microservices demo
|
||||||
|
|
||||||
|
This project contains a 10-tier microservices application. The application is a
|
||||||
|
web-based e-commerce app called “Hipster Shop” where users can browse items,
|
||||||
|
add them to the cart, and purchase them.
|
||||||
|
|
||||||
|
### Setup on GKE
|
||||||
|
|
||||||
|
1. Install:
|
||||||
|
|
||||||
|
- [gcloud](https://cloud.google.com/sdk/) + sign in to your account/project.
|
||||||
|
- kubectl (can be installed via `gcloud components install kubectl`)
|
||||||
|
- Docker (on Mac/Windows, install Docker for Desktop CE)
|
||||||
|
- [Skaffold](https://github.com/GoogleContainerTools/skaffold/#installation)
|
||||||
|
|
||||||
|
1. Create a Google Kubernetes Engine cluster and make sure `kubectl` is pointing
|
||||||
|
to the cluster.
|
||||||
|
|
||||||
|
1. Enable Google Container Registry (GCR) on your GCP project:
|
||||||
|
|
||||||
|
gcloud services enable containerregistry.googleapis.com
|
||||||
|
|
||||||
|
1. Configure docker to authenticate to GCR:
|
||||||
|
|
||||||
|
gcloud auth configure-docker -q
|
||||||
|
|
||||||
|
1. Edit `skaffold.yaml`, prepend your GCR registry host (`gcr.io/YOUR_PROJECT/`)
|
||||||
|
to all `imageName:` fields.
|
||||||
|
|
||||||
|
1. Edit the Deployment manifests at `kubernetes-manifests` directory and update
|
||||||
|
the `image` fields to match the changes you made in the previous step.
|
||||||
|
|
||||||
|
1. Run `skaffold run`. This builds the container
|
||||||
|
images, pushes them to GFR, and deploys the application to Kubernetes.
|
||||||
|
|
||||||
|
1. Find the IP address of your application:
|
||||||
|
|
||||||
|
kubectl get service frontend-external
|
||||||
|
|
||||||
|
then visit the application on your browser to confirm
|
||||||
|
installation.
|
||||||
|
|
|
@ -8,6 +8,20 @@ spec:
|
||||||
labels:
|
labels:
|
||||||
app: cartservice
|
app: cartservice
|
||||||
spec:
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 5
|
||||||
|
initContainers:
|
||||||
|
- name: wait-redis
|
||||||
|
image: redis:alpine
|
||||||
|
command: ['sh', '-c', 'set -x;
|
||||||
|
until timeout -t 5 redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" ping; do
|
||||||
|
echo "waiting for redis at ${REDIS_HOST}:${REDIS_PORT}...";
|
||||||
|
sleep 2;
|
||||||
|
done;']
|
||||||
|
env:
|
||||||
|
- name: REDIS_HOST
|
||||||
|
value: "redis-cart"
|
||||||
|
- name: REDIS_PORT
|
||||||
|
value: "6379"
|
||||||
containers:
|
containers:
|
||||||
- name: server
|
- name: server
|
||||||
image: cartservice
|
image: cartservice
|
||||||
|
@ -20,10 +34,10 @@ spec:
|
||||||
value: "7070"
|
value: "7070"
|
||||||
- name: LISTEN_ADDR
|
- name: LISTEN_ADDR
|
||||||
value: "0.0.0.0"
|
value: "0.0.0.0"
|
||||||
- name: GRPC_TRACE
|
# - name: GRPC_TRACE
|
||||||
value: "all"
|
# value: "all"
|
||||||
- name: GRPC_VERBOSITY
|
# - name: GRPC_VERBOSITY
|
||||||
value: "debug"
|
# value: "debug"
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 200m
|
cpu: 200m
|
||||||
|
|
|
@ -13,15 +13,15 @@ spec:
|
||||||
- name: server
|
- name: server
|
||||||
image: currencyservice
|
image: currencyservice
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 31337
|
- containerPort: 7000
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
tcpSocket:
|
tcpSocket:
|
||||||
port: 31337
|
port: 7000
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
tcpSocket:
|
tcpSocket:
|
||||||
port: 31337
|
port: 7000
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 100m
|
cpu: 100m
|
||||||
|
@ -40,4 +40,4 @@ spec:
|
||||||
app: currencyservice
|
app: currencyservice
|
||||||
ports:
|
ports:
|
||||||
- port: 7000
|
- port: 7000
|
||||||
targetPort: 31337
|
targetPort: 7000
|
||||||
|
|
|
@ -71,5 +71,5 @@ spec:
|
||||||
selector:
|
selector:
|
||||||
app: frontend
|
app: frontend
|
||||||
ports:
|
ports:
|
||||||
- port: 8081
|
- port: 80
|
||||||
targetPort: 8080
|
targetPort: 8080
|
||||||
|
|
39
kubernetes-manifests/loadgenerator.yaml
Normal file
39
kubernetes-manifests/loadgenerator.yaml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: loadgenerator
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: loadgenerator
|
||||||
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 5
|
||||||
|
restartPolicy: Always
|
||||||
|
initContainers:
|
||||||
|
- name: wait-frontend
|
||||||
|
image: alpine:3.6
|
||||||
|
command: ['sh', '-c', 'set -x; apk add --no-cache curl;
|
||||||
|
until curl -f "http://${FRONTEND_ADDR}"; do
|
||||||
|
echo "waiting for http://${FRONTEND_ADDR}";
|
||||||
|
sleep 2;
|
||||||
|
done;']
|
||||||
|
env:
|
||||||
|
- name: FRONTEND_ADDR
|
||||||
|
value: "frontend:80"
|
||||||
|
containers:
|
||||||
|
- name: main
|
||||||
|
image: loadgenerator
|
||||||
|
env:
|
||||||
|
- name: FRONTEND_ADDR
|
||||||
|
value: "frontend:80"
|
||||||
|
- name: USERS
|
||||||
|
value: "1"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 128Mi
|
|
@ -22,6 +22,8 @@ build:
|
||||||
workspace: src/cartservice
|
workspace: src/cartservice
|
||||||
- imageName: frontend
|
- imageName: frontend
|
||||||
workspace: src/frontend
|
workspace: src/frontend
|
||||||
|
- imageName: loadgenerator
|
||||||
|
workspace: src/loadgenerator
|
||||||
deploy:
|
deploy:
|
||||||
kubectl:
|
kubectl:
|
||||||
manifests:
|
manifests:
|
||||||
|
|
|
@ -3,5 +3,5 @@ WORKDIR /usr/src/app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install --only=production
|
RUN npm install --only=production
|
||||||
COPY . .
|
COPY . .
|
||||||
EXPOSE 31337
|
EXPOSE 7000
|
||||||
CMD [ "node", "server.js" ]
|
CMD [ "node", "server.js" ]
|
|
@ -20,7 +20,7 @@ const request = require('request');
|
||||||
const xml2js = require('xml2js');
|
const xml2js = require('xml2js');
|
||||||
|
|
||||||
const PROTO_PATH = path.join(__dirname, './proto/demo.proto');
|
const PROTO_PATH = path.join(__dirname, './proto/demo.proto');
|
||||||
const PORT = 31337;
|
const PORT = 7000;
|
||||||
const DATA_URL = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml';
|
const DATA_URL = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml';
|
||||||
const shopProto = grpc.load(PROTO_PATH).hipstershop;
|
const shopProto = grpc.load(PROTO_PATH).hipstershop;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ WORKDIR /go/src/frontend
|
||||||
RUN go get -d github.com/google/uuid \
|
RUN go get -d github.com/google/uuid \
|
||||||
github.com/gorilla/mux \
|
github.com/gorilla/mux \
|
||||||
github.com/pkg/errors \
|
github.com/pkg/errors \
|
||||||
|
github.com/sirupsen/logrus \
|
||||||
google.golang.org/grpc \
|
google.golang.org/grpc \
|
||||||
google.golang.org/grpc/codes \
|
google.golang.org/grpc/codes \
|
||||||
google.golang.org/grpc/status
|
google.golang.org/grpc/status
|
||||||
|
@ -22,7 +23,8 @@ RUN go install .
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
FROM alpine as release
|
FROM alpine as release
|
||||||
RUN apk add --no-cache ca-certificates
|
RUN apk add --no-cache ca-certificates \
|
||||||
|
busybox-extras net-tools bind-tools
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
COPY --from=builder /go/bin/frontend /frontend/server
|
COPY --from=builder /go/bin/frontend /frontend/server
|
||||||
COPY ./templates ./templates
|
COPY ./templates ./templates
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"frontend/money"
|
"frontend/money"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
pb "frontend/genproto"
|
pb "frontend/genproto"
|
||||||
)
|
)
|
||||||
|
@ -24,45 +22,22 @@ var (
|
||||||
}).ParseGlob("templates/*.html"))
|
}).ParseGlob("templates/*.html"))
|
||||||
)
|
)
|
||||||
|
|
||||||
func ensureSessionID(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var sessionID string
|
|
||||||
c, err := r.Cookie(cookieSessionID)
|
|
||||||
if err == http.ErrNoCookie {
|
|
||||||
u, _ := uuid.NewRandom()
|
|
||||||
sessionID = u.String()
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: cookieSessionID,
|
|
||||||
Value: sessionID,
|
|
||||||
MaxAge: cookieMaxAge,
|
|
||||||
})
|
|
||||||
} else if err != nil {
|
|
||||||
renderHTTPError(w, errors.Wrap(err, "unrecognized cookie error"), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
sessionID = c.Value
|
|
||||||
}
|
|
||||||
ctx := context.WithValue(r.Context(), ctxKeySessionID{}, sessionID)
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
next(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("[home] session_id=%s currency=%s", sessionID(r), currentCurrency(r))
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||||
|
log.WithField("currency", currentCurrency(r)).Info("home")
|
||||||
currencies, err := fe.getCurrencies(r.Context())
|
currencies, err := fe.getCurrencies(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
products, err := fe.getProducts(r.Context())
|
products, err := fe.getProducts(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "could not retrieve products"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "could not retrieve products"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cart, err := fe.getCart(r.Context(), sessionID(r))
|
cart, err := fe.getCart(r.Context(), sessionID(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +49,7 @@ func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
for i, p := range products {
|
for i, p := range products {
|
||||||
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrapf(err, "failed to do currency conversion for product %s", p.GetId()), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrapf(err, "failed to do currency conversion for product %s", p.GetId()), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ps[i] = productView{p, price}
|
ps[i] = productView{p, price}
|
||||||
|
@ -87,43 +62,46 @@ func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
"session_id": sessionID(r),
|
"session_id": sessionID(r),
|
||||||
"cart_size": len(cart),
|
"cart_size": len(cart),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Println(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||||
id := mux.Vars(r)["id"]
|
id := mux.Vars(r)["id"]
|
||||||
if id == "" {
|
if id == "" {
|
||||||
renderHTTPError(w, errors.New("product id not specified"), http.StatusBadRequest)
|
renderHTTPError(log, w, errors.New("product id not specified"), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("[productHandler] id=%s currency=%s session=%s", id, currentCurrency(r), sessionID(r))
|
log.WithField("id", id).WithField("currency", currentCurrency(r)).
|
||||||
|
Debug("serving product page")
|
||||||
|
|
||||||
p, err := fe.getProduct(r.Context(), id)
|
p, err := fe.getProduct(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currencies, err := fe.getCurrencies(r.Context())
|
currencies, err := fe.getCurrencies(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cart, err := fe.getCart(r.Context(), sessionID(r))
|
cart, err := fe.getCart(r.Context(), sessionID(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "failed to convert currency"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "failed to convert currency"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), []string{id})
|
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), []string{id})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "failed to get product recommendations"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "failed to get product recommendations"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,22 +123,23 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||||
quantity, _ := strconv.ParseUint(r.FormValue("quantity"), 10, 32)
|
quantity, _ := strconv.ParseUint(r.FormValue("quantity"), 10, 32)
|
||||||
productID := r.FormValue("product_id")
|
productID := r.FormValue("product_id")
|
||||||
if productID == "" || quantity == 0 {
|
if productID == "" || quantity == 0 {
|
||||||
renderHTTPError(w, errors.New("invalid form input"), http.StatusBadRequest)
|
renderHTTPError(log, w, errors.New("invalid form input"), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("[addToCart] product_id=%s qty=%d session_id=%s", productID, quantity, sessionID(r))
|
log.WithField("product", productID).WithField("quantity", quantity).Debug("adding to cart")
|
||||||
|
|
||||||
p, err := fe.getProduct(r.Context(), productID)
|
p, err := fe.getProduct(r.Context(), productID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fe.insertCart(r.Context(), sessionID(r), p.GetId(), int32(quantity)); err != nil {
|
if err := fe.insertCart(r.Context(), sessionID(r), p.GetId(), int32(quantity)); err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "failed to add to cart"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "failed to add to cart"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("location", "/cart")
|
w.Header().Set("location", "/cart")
|
||||||
|
@ -168,10 +147,11 @@ func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("[emptyCart] session_id=%s", sessionID(r))
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||||
|
log.Debug("emptying cart")
|
||||||
|
|
||||||
if err := fe.emptyCart(r.Context(), sessionID(r)); err != nil {
|
if err := fe.emptyCart(r.Context(), sessionID(r)); err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "failed to empty cart"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "failed to empty cart"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("location", "/")
|
w.Header().Set("location", "/")
|
||||||
|
@ -179,27 +159,28 @@ func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("[viewCart] session_id=%s", sessionID(r))
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||||
|
log.Debug("view user cart")
|
||||||
currencies, err := fe.getCurrencies(r.Context())
|
currencies, err := fe.getCurrencies(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cart, err := fe.getCart(r.Context(), sessionID(r))
|
cart, err := fe.getCart(r.Context(), sessionID(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), cartIDs(cart))
|
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), cartIDs(cart))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "failed to get product recommendations"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "failed to get product recommendations"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
shippingCost, err := fe.getShippingQuote(r.Context(), cart, currentCurrency(r))
|
shippingCost, err := fe.getShippingQuote(r.Context(), cart, currentCurrency(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "failed to get shipping quote"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "failed to get shipping quote"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,12 +194,12 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request
|
||||||
for i, item := range cart {
|
for i, item := range cart {
|
||||||
p, err := fe.getProduct(r.Context(), item.GetProductId())
|
p, err := fe.getProduct(r.Context(), item.GetProductId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrapf(err, "could not retrieve product #%s", item.GetProductId()), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrapf(err, "could not retrieve product #%s", item.GetProductId()), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrapf(err, "could not convert currency for product #%s", item.GetProductId()), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrapf(err, "could not convert currency for product #%s", item.GetProductId()), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,7 +229,8 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("[checkout] session_id=%s", sessionID(r))
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||||
|
log.Debug("placing order")
|
||||||
|
|
||||||
var (
|
var (
|
||||||
email = r.FormValue("email")
|
email = r.FormValue("email")
|
||||||
|
@ -281,10 +263,10 @@ func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Reque
|
||||||
Country: country},
|
Country: country},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderHTTPError(w, errors.Wrap(err, "failed to complete the order"), http.StatusInternalServerError)
|
renderHTTPError(log, w, errors.Wrap(err, "failed to complete the order"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("order #%s completed", order.GetOrder().GetOrderId())
|
log.WithField("order", order.GetOrder().GetOrderId()).Info("order placed")
|
||||||
|
|
||||||
order.GetOrder().GetItems()
|
order.GetOrder().GetItems()
|
||||||
recommendations, _ := fe.getRecommendations(r.Context(), sessionID(r), nil)
|
recommendations, _ := fe.getRecommendations(r.Context(), sessionID(r), nil)
|
||||||
|
@ -306,7 +288,8 @@ func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Reque
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("[logout] session_id=%s", sessionID(r))
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||||
|
log.Debug("logging out")
|
||||||
for _, c := range r.Cookies() {
|
for _, c := range r.Cookies() {
|
||||||
c.Expires = time.Now().Add(-time.Hour * 24 * 365)
|
c.Expires = time.Now().Add(-time.Hour * 24 * 365)
|
||||||
c.MaxAge = -1
|
c.MaxAge = -1
|
||||||
|
@ -317,8 +300,11 @@ func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Request) {
|
func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
|
||||||
cur := r.FormValue("currency_code")
|
cur := r.FormValue("currency_code")
|
||||||
log.Printf("[setCurrency] session_id=%s code=%s", sessionID(r), cur)
|
log.WithField("curr.new", cur).WithField("curr.old", currentCurrency(r)).
|
||||||
|
Debug("setting currency")
|
||||||
|
|
||||||
if cur != "" {
|
if cur != "" {
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: cookieCurrency,
|
Name: cookieCurrency,
|
||||||
|
@ -334,8 +320,8 @@ func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Requ
|
||||||
w.WriteHeader(http.StatusFound)
|
w.WriteHeader(http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderHTTPError(w http.ResponseWriter, err error, code int) {
|
func renderHTTPError(log logrus.FieldLogger, w http.ResponseWriter, err error, code int) {
|
||||||
log.Printf("[error](code=%d) %+v ", code, err)
|
log.WithField("error", err).Error("request error")
|
||||||
errMsg := fmt.Sprintf("%+v", err)
|
errMsg := fmt.Sprintf("%+v", err)
|
||||||
|
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
|
|
|
@ -3,14 +3,13 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,7 +52,9 @@ type frontendServer struct {
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
log.SetFlags(log.Lshortfile | log.Ltime)
|
log := logrus.New()
|
||||||
|
log.Level = logrus.DebugLevel
|
||||||
|
log.Formatter = &logrus.TextFormatter{}
|
||||||
|
|
||||||
srvPort := port
|
srvPort := port
|
||||||
if os.Getenv("PORT") != "" {
|
if os.Getenv("PORT") != "" {
|
||||||
|
@ -76,17 +77,20 @@ func main() {
|
||||||
mustConnGRPC(ctx, &svc.checkoutSvcConn, svc.checkoutSvcAddr)
|
mustConnGRPC(ctx, &svc.checkoutSvcConn, svc.checkoutSvcAddr)
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.HandleFunc("/", ensureSessionID(svc.homeHandler)).Methods(http.MethodGet, http.MethodHead)
|
r.HandleFunc("/", svc.homeHandler).Methods(http.MethodGet, http.MethodHead)
|
||||||
r.HandleFunc("/product/{id}", ensureSessionID(svc.productHandler)).Methods(http.MethodGet, http.MethodHead)
|
r.HandleFunc("/product/{id}", svc.productHandler).Methods(http.MethodGet, http.MethodHead)
|
||||||
r.HandleFunc("/cart", ensureSessionID(svc.viewCartHandler)).Methods(http.MethodGet, http.MethodHead)
|
r.HandleFunc("/cart", svc.viewCartHandler).Methods(http.MethodGet, http.MethodHead)
|
||||||
r.HandleFunc("/cart", ensureSessionID(svc.addToCartHandler)).Methods(http.MethodPost)
|
r.HandleFunc("/cart", svc.addToCartHandler).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/cart/empty", ensureSessionID(svc.emptyCartHandler)).Methods(http.MethodPost)
|
r.HandleFunc("/cart/empty", svc.emptyCartHandler).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/setCurrency", ensureSessionID(svc.setCurrencyHandler)).Methods(http.MethodPost)
|
r.HandleFunc("/setCurrency", svc.setCurrencyHandler).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/logout", svc.logoutHandler).Methods(http.MethodGet)
|
r.HandleFunc("/logout", svc.logoutHandler).Methods(http.MethodGet)
|
||||||
r.HandleFunc("/cart/checkout", ensureSessionID(svc.placeOrderHandler)).Methods(http.MethodPost)
|
r.HandleFunc("/cart/checkout", svc.placeOrderHandler).Methods(http.MethodPost)
|
||||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
||||||
log.Printf("starting server on " + addr + ":" + srvPort)
|
|
||||||
log.Fatal(http.ListenAndServe(addr+":"+srvPort, r))
|
log.Infof("starting server on " + addr + ":" + srvPort)
|
||||||
|
loggedHandler := &logHandler{log: log, next: r}
|
||||||
|
log.Fatal(http.ListenAndServe(addr+":"+srvPort,
|
||||||
|
http.HandlerFunc(ensureSessionID(loggedHandler))))
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustMapEnv(target *string, envKey string) {
|
func mustMapEnv(target *string, envKey string) {
|
||||||
|
|
91
src/frontend/middleware.go
Normal file
91
src/frontend/middleware.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ctxKeyLog struct{}
|
||||||
|
type ctxKeyRequestID struct{}
|
||||||
|
|
||||||
|
type logHandler struct {
|
||||||
|
log *logrus.Logger
|
||||||
|
next http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseRecorder struct {
|
||||||
|
b int
|
||||||
|
status int
|
||||||
|
w http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *responseRecorder) Header() http.Header { return r.w.Header() }
|
||||||
|
|
||||||
|
func (r *responseRecorder) Write(p []byte) (int, error) {
|
||||||
|
if r.status == 0 {
|
||||||
|
r.status = http.StatusOK
|
||||||
|
}
|
||||||
|
n, err := r.w.Write(p)
|
||||||
|
r.b += n
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *responseRecorder) WriteHeader(statusCode int) {
|
||||||
|
r.status = statusCode
|
||||||
|
r.w.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lh *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
requestID, _ := uuid.NewRandom()
|
||||||
|
ctx = context.WithValue(ctx, ctxKeyRequestID{}, requestID.String())
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
rr := &responseRecorder{w: w}
|
||||||
|
log := lh.log.WithFields(logrus.Fields{
|
||||||
|
"http.req.path": r.URL.Path,
|
||||||
|
"http.req.method": r.Method,
|
||||||
|
"http.req.id": requestID.String(),
|
||||||
|
})
|
||||||
|
if v, ok := r.Context().Value(ctxKeySessionID{}).(string); ok {
|
||||||
|
log = log.WithField("session", v)
|
||||||
|
}
|
||||||
|
log.Debug("request started")
|
||||||
|
defer func() {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"http.resp.took_ms": int64(time.Since(start) / time.Millisecond),
|
||||||
|
"http.resp.status": rr.status,
|
||||||
|
"http.resp.bytes": rr.b}).Debugf("request complete")
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, ctxKeyLog{}, log)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
lh.next.ServeHTTP(rr, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureSessionID(next http.Handler) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var sessionID string
|
||||||
|
c, err := r.Cookie(cookieSessionID)
|
||||||
|
if err == http.ErrNoCookie {
|
||||||
|
u, _ := uuid.NewRandom()
|
||||||
|
sessionID = u.String()
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: cookieSessionID,
|
||||||
|
Value: sessionID,
|
||||||
|
MaxAge: cookieMaxAge,
|
||||||
|
})
|
||||||
|
} else if err != nil {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
sessionID = c.Value
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(r.Context(), ctxKeySessionID{}, sessionID)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
kubectl port-forward $(kubectl get pods -l app=currencyservice -o=name | head -n 1) 7000:31337 &
|
kubectl port-forward $(kubectl get pods -l app=currencyservice -o=name | head -n 1) 7000:7000 &
|
||||||
kubectl port-forward $(kubectl get pods -l app=recommendationservice -o=name | head -n 1) 8081:8080 &
|
kubectl port-forward $(kubectl get pods -l app=recommendationservice -o=name | head -n 1) 8081:8080 &
|
||||||
kubectl port-forward $(kubectl get pods -l app=cartservice -o=name | head -n 1) 7070:7070 &
|
kubectl port-forward $(kubectl get pods -l app=cartservice -o=name | head -n 1) 7070:7070 &
|
||||||
kubectl port-forward $(kubectl get pods -l app=productcatalogservice -o=name | head -n 1) 3550:3550 &
|
kubectl port-forward $(kubectl get pods -l app=productcatalogservice -o=name | head -n 1) 3550:3550 &
|
||||||
|
|
7
src/loadgenerator/Dockerfile
Normal file
7
src/loadgenerator/Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
FROM python:3
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
ENTRYPOINT ./loadgen.sh
|
10
src/loadgenerator/loadgen.sh
Executable file
10
src/loadgenerator/loadgen.sh
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ -z "${FRONTEND_ADDR}" ]]; then
|
||||||
|
echo >&2 "FRONTEND_ADDR not specified"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -x
|
||||||
|
locust --host="http://${FRONTEND_ADDR}" --no-web -c "${USERS:-10}"
|
66
src/loadgenerator/locustfile.py
Normal file
66
src/loadgenerator/locustfile.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import random
|
||||||
|
from locust import HttpLocust, TaskSet
|
||||||
|
|
||||||
|
products = [
|
||||||
|
'0PUK6V6EV0',
|
||||||
|
'1YMWWN1N4O',
|
||||||
|
'2ZYFJ3GM2N',
|
||||||
|
'66VCHSJNUP',
|
||||||
|
'6E92ZMYYFZ',
|
||||||
|
'9SIQT8TOJO',
|
||||||
|
'L9ECAV7KIM',
|
||||||
|
'LS4PSXUNUM',
|
||||||
|
'OLJCESPC7Z']
|
||||||
|
|
||||||
|
def index(l):
|
||||||
|
l.client.get("/")
|
||||||
|
|
||||||
|
def setCurrency(l):
|
||||||
|
currencies = ['EUR', 'USD', 'JPY', 'CAD']
|
||||||
|
l.client.post("/setCurrency",
|
||||||
|
{'currency_code': random.choice(currencies)})
|
||||||
|
|
||||||
|
def browseProduct(l):
|
||||||
|
l.client.get("/product/" + random.choice(products))
|
||||||
|
|
||||||
|
def viewCart(l):
|
||||||
|
l.client.get("/cart")
|
||||||
|
|
||||||
|
def addToCart(l):
|
||||||
|
product = random.choice(products)
|
||||||
|
l.client.get("/product/" + product)
|
||||||
|
l.client.post("/cart", {
|
||||||
|
'product_id': product,
|
||||||
|
'quantity': random.choice([1,2,3,4,5,10])})
|
||||||
|
|
||||||
|
def checkout(l):
|
||||||
|
addToCart(l)
|
||||||
|
l.client.post("/cart/checkout", {
|
||||||
|
'email': 'someone@example.com',
|
||||||
|
'street_address': '1600 Amphitheatre Parkway',
|
||||||
|
'zip_code': '94043',
|
||||||
|
'city': 'Mountain View',
|
||||||
|
'state': 'CA',
|
||||||
|
'country': 'United States',
|
||||||
|
'credit_card_number': '4432-8015-6152-0454',
|
||||||
|
'credit_card_expiration_month': '1',
|
||||||
|
'credit_card_expiration_year': '2019',
|
||||||
|
'credit_card_cvv': '672',
|
||||||
|
})
|
||||||
|
|
||||||
|
class UserBehavior(TaskSet):
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
|
index(self)
|
||||||
|
|
||||||
|
tasks = {index: 1,
|
||||||
|
setCurrency: 2,
|
||||||
|
browseProduct: 10,
|
||||||
|
addToCart: 2,
|
||||||
|
viewCart: 3,
|
||||||
|
checkout: 1}
|
||||||
|
|
||||||
|
class WebsiteUser(HttpLocust):
|
||||||
|
task_set = UserBehavior
|
||||||
|
min_wait = 1000
|
||||||
|
max_wait = 10000
|
2
src/loadgenerator/requirements.txt
Normal file
2
src/loadgenerator/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
locustio==0.8.1
|
||||||
|
pyzmq==17.0.0
|
|
@ -22,6 +22,8 @@ type server struct{}
|
||||||
|
|
||||||
// GetQuote produces a shipping quote (cost) in USD.
|
// GetQuote produces a shipping quote (cost) in USD.
|
||||||
func (s *server) GetQuote(ctx context.Context, in *pb.GetQuoteRequest) (*pb.GetQuoteResponse, error) {
|
func (s *server) GetQuote(ctx context.Context, in *pb.GetQuoteRequest) (*pb.GetQuoteResponse, error) {
|
||||||
|
log.Printf("[GetQuote] received request")
|
||||||
|
defer log.Printf("[GetQuote] completed request")
|
||||||
|
|
||||||
// 1. Our quote system requires the total number of items to be shipped.
|
// 1. Our quote system requires the total number of items to be shipped.
|
||||||
count := 0
|
count := 0
|
||||||
|
@ -45,6 +47,8 @@ func (s *server) GetQuote(ctx context.Context, in *pb.GetQuoteRequest) (*pb.GetQ
|
||||||
// ShipOrder mocks that the requested items will be shipped.
|
// ShipOrder mocks that the requested items will be shipped.
|
||||||
// It supplies a tracking ID for notional lookup of shipment delivery status.
|
// It supplies a tracking ID for notional lookup of shipment delivery status.
|
||||||
func (s *server) ShipOrder(ctx context.Context, in *pb.ShipOrderRequest) (*pb.ShipOrderResponse, error) {
|
func (s *server) ShipOrder(ctx context.Context, in *pb.ShipOrderRequest) (*pb.ShipOrderResponse, error) {
|
||||||
|
log.Printf("[ShipOrder] received request")
|
||||||
|
defer log.Printf("[ShipOrder] completed request")
|
||||||
// 1. Create a Tracking ID
|
// 1. Create a Tracking ID
|
||||||
baseAddress := fmt.Sprintf("%s, %s, %s", in.Address.StreetAddress, in.Address.City, in.Address.State)
|
baseAddress := fmt.Sprintf("%s, %s, %s", in.Address.StreetAddress, in.Address.City, in.Address.State)
|
||||||
id := CreateTrackingId(baseAddress)
|
id := CreateTrackingId(baseAddress)
|
||||||
|
|
Loading…
Reference in a new issue