Skip to content
This repository has been archived by the owner on Mar 23, 2024. It is now read-only.

Commit

Permalink
Remove Helper & Use Dapr SDK (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
benc-uk authored Jul 2, 2022
1 parent 4030e16 commit 1a3ffcc
Show file tree
Hide file tree
Showing 14 changed files with 439 additions and 443 deletions.
15 changes: 8 additions & 7 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,22 @@ jobs:

- name: "Install extra tools"
run: |
go install gotest.tools/gotestsum@latest
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
- name: "Check code & lint"
run: make lint

- name: "Run all unit tests"
run: make test
run: make test-reports

# Very optional - upload of test results
# - name: "Upload test reports to Azure"
# if: ${{ success() || failure() }}
# run: |
# az storage blob upload-batch --account-name $STORAGE_ACCT_NAME --account-key "${{ secrets.STORAGE_KEY }}" \
# --source ./output --destination \$web/${{ github.run_id }} --no-progress > /dev/null
# echo -e "📜🌍 Test reports uploaded and viewable here - https://$STORAGE_ACCT_NAME.z6.web.core.windows.net/${{ github.run_id }}/"
- name: "Upload test reports to Azure"
if: ${{ success() || failure() }}
run: |
az storage blob upload-batch --account-name $STORAGE_ACCT_NAME --account-key "${{ secrets.STORAGE_KEY }}" \
--source ./output --destination \$web/${{ github.run_id }} --no-progress > /dev/null
echo -e "📜🌍 Test reports uploaded and viewable here - https://$STORAGE_ACCT_NAME.z6.web.core.windows.net/${{ github.run_id }}/"
# ===== Build container images ======
build-images:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
SERVICE_DIR := cmd
FRONTEND_DIR := web/frontend
OUTPUT_DIR := ./output
VERSION ?= 0.6.0
VERSION ?= 0.7.0
BUILD_INFO ?= "Makefile build"
DAPR_RUN_LOGLEVEL := warn

Expand Down
17 changes: 6 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ The backend microservices are written in Go (however it's worth nothing that Dap
This repo is a monorepo, containing the source for several discreet but closely linked codebases, one for each component of the project, as described below.
The ["Go Standard Project Layout"](https://github.com/golang-standards/project-layout) has been used.

:warning: The project is still in the experimental stage, with a high rate of change - expect breaking changes

# Architecture

The following diagram shows all the components of the application and main interactions. It also highlights which Dapr API/feature (aka Dapr building block) is used and where.
Expand All @@ -23,7 +21,7 @@ The application uses the following [Dapr Building Blocks](https://docs.dapr.io/d
- **Service Invocation** — The API gateway calls the four main microservices using HTTP calls to [Dapr service invocation](https://docs.dapr.io/developing-applications/building-blocks/service-invocation/service-invocation-overview/). This provides retries, mTLS and service discovery.
- **State** — State is held for users and orders using the [Dapr state management API](https://docs.dapr.io/developing-applications/building-blocks/state-management/state-management-overview/). The state provider used is Redis, however any other provider could be plugged in without any application code changes.
- **Pub/Sub** — The submission of new orders through the cart service, is decoupled from the order processing via pub/sub messaging and the [Dapr pub/sub messaging API](https://docs.dapr.io/developing-applications/building-blocks/pubsub/pubsub-overview/). New orders are placed on a topic as messages, to be collected by the orders service. This allows the orders service to independently scale and separates our reads & writes
- **Output Bindings** — To communicate with downstream & 3rd party systems, the [Dapr Bindings API](https://docs.dapr.io/developing-applications/building-blocks/bindings/bindings-overview/) is used. This allows the store to carry out tasks such as saving order details into external storage (e.g. Azure Blob) and notify uses with emails via SendGrid
- **Output Bindings** — To communicate with downstream & 3rd party systems, the [Dapr Bindings API](https://docs.dapr.io/developing-applications/building-blocks/bindings/bindings-overview/) is used. This allows the system to carry out tasks such as saving order details into external storage (e.g. Azure Blob) and notify uses with emails via SendGrid
- **Middleware** — Dapr supports a range of HTTP middleware, for this project traffic rate limiting can enabled on any of the APIs with a single Kubernetes annotation

# Project Status
Expand All @@ -44,7 +42,6 @@ Shared Go code lives in the `pkg/` directory, which is used by all the services,
- `pkg/api` - A base API extended by all services, provides health & status endpoints.
- `pkg/apitests` - A simple helper for running sets of router/API based tests.
- `pkg/auth` - Server side token validation of JWT using JWK.
- `pkg/dapr` - A Dapr helper & wrapper library for state, pub/sub and output bindings
- `pkg/env` - Very simple `os.LookupEnv` wrapper with fallback defaults.
- `pkg/problem` - Standarized REST error messages using [RFC 7807 Problem Details](https://tools.ietf.org/html/rfc7807).

Expand All @@ -69,7 +66,7 @@ For testing:
This service provides order processing to the Dapr Store.
It is written in Go, source is in `cmd/orders` and it exposes the following API routes:

```
```text
/get/{id} GET a single order by orderID
/getForUser/{username} GET all orders for a given username
```
Expand Down Expand Up @@ -130,12 +127,11 @@ None directly, but is called via service invocation from other services, the API
This provides a cart service to the Dapr Store. The currently implementation is a MVP.
It is written in Go, source is in `cmd/cart` and it exposes the following API routes:

```
```text
/setProduct/{username}/{productId}/{count} PUT a number of products in the cart of given user
/get/{username} GET cart for user
/submit POST submit a cart, and turn it into an 'Order'
/clear/{username} PUT clear a user's cart
```

See `pkg/models` for details of the **Order** struct.
Expand Down Expand Up @@ -196,8 +192,8 @@ This is a (very) basic guide to running Dapr Store locally. Only instructions fo
### Prereqs

- Docker
- Go v1.15+
- Node.js v12+
- Go v1.18+
- Node.js v14+

### Setup

Expand Down Expand Up @@ -236,7 +232,6 @@ lint 🔎 Lint & format, check to be run in CI, sets exit code o
lint-fix 📝 Lint & format, fixes errors and modifies code
test 🎯 Unit tests for services and snapshot tests for SPA frontend
test-reports 📜 Unit tests with coverage and test reports
test-snapshot 📷 Update snapshots for frontend tests
image-all 📦 Build all container images
push-all 📤 Push all images to registry
bundle 💻 Build and bundle the frontend Vue SPA
Expand All @@ -247,7 +242,7 @@ stop ⛔ Stop & kill everything started locally from `make run`

# CI / CD

A working set of CI and CD release GitHub Actions workflows are provided `.github/workflows/`, automated builds are run in GitHub hosted runners
A set of CI and CD release GitHub Actions workflows are included in `.github/workflows/`, automated builds are run in GitHub hosted runners

### [GitHub Actions](https://github.com/benc-uk/dapr-store/actions)

Expand Down
92 changes: 55 additions & 37 deletions cmd/cart/impl/impl.go
Original file line number Diff line number Diff line change
@@ -1,72 +1,79 @@
package impl

import (
"context"
"encoding/json"
"log"
"math/rand"
"os"
"time"

cartspec "github.com/benc-uk/dapr-store/cmd/cart/spec"
orderspec "github.com/benc-uk/dapr-store/cmd/orders/spec"
productspec "github.com/benc-uk/dapr-store/cmd/products/spec"
"github.com/benc-uk/dapr-store/pkg/dapr"

// "github.com/benc-uk/dapr-store/pkg/dapr"
"github.com/benc-uk/dapr-store/pkg/env"
"github.com/benc-uk/dapr-store/pkg/problem"
dapr "github.com/dapr/go-sdk/client"
)

// CartService is a Dapr implementation of CartService interface
type CartService struct {
*dapr.Helper
pubSubName string
topicName string
storeName string
pubSubName string
topicName string
storeName string
serviceName string
client dapr.Client
}

//
// NewService creates a new OrderService
// NewService creates a new CartService
//
func NewService(serviceName string) *CartService {
// Set up Dapr & checks for Dapr sidecar port, abort
helper := dapr.NewHelper(serviceName)
if helper == nil {
os.Exit(1)
}
topicName := env.GetEnvString("DAPR_ORDERS_TOPIC", "orders-queue")
storeName := env.GetEnvString("DAPR_STORE_NAME", "statestore")
pubSubName := env.GetEnvString("DAPR_PUBSUB_NAME", "pubsub")

log.Printf("### Dapr pub/sub topic name: %s\n", topicName)
log.Printf("### Dapr state store name: %s\n", storeName)

// Set up Dapr client & checks for Dapr sidecar, otherwise die
client, err := dapr.NewClient()
if err != nil {
log.Panicln("FATAL! Dapr process/sidecar NOT found. Terminating!")
}

return &CartService{
helper,
pubSubName,
topicName,
storeName,
serviceName,
client,
}
}

//
// Get fetches saved cart for a given user, if not exists an empty cart is returned
//
func (s CartService) Get(username string) (*cartspec.Cart, error) {
data, prob := s.GetState(s.storeName, username)
if prob != nil {
return nil, prob
//data, prob := s.GetState(s.storeName, username)
data, err := s.client.GetState(context.Background(), s.storeName, username, nil)
if err != nil {
return nil, problem.NewDaprStateProblem(err, s.serviceName)
}

if len(data) <= 0 {
// Create an empty cart
if data.Value == nil {
cart := &cartspec.Cart{}
cart.ForUser = username
cart.Products = make(map[string]int)
return cart, nil
}

cart := &cartspec.Cart{}
err := json.Unmarshal(data, cart)
err = json.Unmarshal(data.Value, cart)
if err != nil {
prob := problem.New("err://json-decode", "Malformed cart JSON", 500, "JSON could not be decoded", s.ServiceName)
prob := problem.New("err://json-decode", "Malformed cart JSON", 500, "JSON could not be decoded", s.serviceName)
return nil, prob
}

Expand All @@ -78,26 +85,28 @@ func (s CartService) Get(username string) (*cartspec.Cart, error) {
//
func (s CartService) Submit(cart cartspec.Cart) (*orderspec.Order, error) {
if len(cart.Products) == 0 {
return nil, problem.New("err://order-cart", "Cart empty", 400, "No items in cart", s.ServiceName)
return nil, problem.New("err://order-cart", "Cart empty", 400, "No items in cart", s.serviceName)
}

// Build up line item array
lineItems := []orderspec.LineItem{}

// Process the cart server side, calculating the order price
// This involves a service to service call to invoke the products service
var orderAmount float32
for productID, count := range cart.Products {
resp, err := s.InvokeGet("products", `get/`+productID)
if err != nil || resp.StatusCode != 200 {
return nil, problem.NewAPIProblem("err://cart-product", "Submit cart, product lookup error "+productID, s.ServiceName, resp, err)
resp, err := s.client.InvokeMethod(context.Background(), "products", `get/`+productID, "get")
if err != nil {
return nil, problem.New500("err://cart-product", "Submit cart, product lookup error "+productID, s.serviceName, nil, err)
}

product := &productspec.Product{}
err = json.NewDecoder(resp.Body).Decode(product)
log.Printf("SUB CART GOT PROD %+v", product)
err = json.Unmarshal(resp, product)
if err != nil {
prob := problem.New("err://json-decode", "Malformed JSON", 500, "Product JSON could not be decoded", s.ServiceName)
prob := problem.New("err://json-decode", "Malformed JSON", 500, "Product JSON could not be decoded", s.serviceName)
return nil, prob
}

lineItem := &orderspec.LineItem{
Product: *product,
Count: count,
Expand All @@ -116,13 +125,14 @@ func (s CartService) Submit(cart cartspec.Cart) (*orderspec.Order, error) {
LineItems: lineItems,
}

prob := s.PublishMessage(s.pubSubName, s.topicName, order)
if prob != nil {
return nil, prob
err := s.client.PublishEvent(context.Background(), s.pubSubName, s.topicName, order)
if err != nil {
return nil, problem.NewDaprPubSubProblem(err, s.serviceName)
}

err := s.Clear(&cart)
err = s.Clear(&cart)
if err != nil {
// Log but don't return the error, as the order was published
log.Printf("### Warning failed to clear cart %s", err)
}

Expand All @@ -134,7 +144,7 @@ func (s CartService) Submit(cart cartspec.Cart) (*orderspec.Order, error) {
//
func (s CartService) SetProductCount(cart *cartspec.Cart, productID string, count int) error {
if count < 0 {
return problem.New("err://invalid-request", "SetProductCount error", 400, "Count can not be negative", s.ServiceName)
return problem.New("err://invalid-request", "SetProductCount error", 400, "Count can not be negative", s.serviceName)
}

if count == 0 {
Expand All @@ -143,9 +153,13 @@ func (s CartService) SetProductCount(cart *cartspec.Cart, productID string, coun
cart.Products[productID] = count
}

prob := s.SaveState(s.storeName, cart.ForUser, cart)
if prob != nil {
return prob
// Call Dapr client to save state
jsonPayload, err := json.Marshal(cart)
if err != nil {
return problem.New500("err://json-marshall", "State JSON marshalling error", s.serviceName, nil, err)
}
if err = s.client.SaveState(context.Background(), s.storeName, cart.ForUser, jsonPayload, nil); err != nil {
return problem.NewDaprStateProblem(err, s.serviceName)
}

return nil
Expand All @@ -156,9 +170,13 @@ func (s CartService) SetProductCount(cart *cartspec.Cart, productID string, coun
//
func (s CartService) Clear(cart *cartspec.Cart) error {
cart.Products = map[string]int{}
prob := s.SaveState(s.storeName, cart.ForUser, cart)
if prob != nil {
return prob
// Call Dapr client to save state
jsonPayload, err := json.Marshal(cart)
if err != nil {
return problem.New500("err://json-marshall", "State JSON marshalling error", s.serviceName, nil, err)
}
if err = s.client.SaveState(context.Background(), s.storeName, cart.ForUser, jsonPayload, nil); err != nil {
return problem.NewDaprStateProblem(err, s.serviceName)
}
return nil
}
Expand Down
7 changes: 6 additions & 1 deletion cmd/cart/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ func (api API) addRoutes(router *mux.Router) {
//
func (api API) setProductCount(resp http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
cart, _ := api.service.Get(vars["username"])
cart, err := api.service.Get(vars["username"])
if err != nil {
prob := err.(problem.Problem)
prob.Send(resp)
return
}

count, err := strconv.Atoi(vars["count"])
if err != nil {
Expand Down
Loading

0 comments on commit 1a3ffcc

Please sign in to comment.