You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-12-24 00:01:31 +02:00
Imported github.com/ardanlabs/service as base example project
This commit is contained in:
137
example-project/cmd/sales-admin/main.go
Normal file
137
example-project/cmd/sales-admin/main.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// This program performs administrative tasks for the garage sale service.
|
||||
//
|
||||
// Run it with --cmd keygen or --cmd useradd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// =========================================================================
|
||||
// Logging
|
||||
|
||||
log := log.New(os.Stdout, "sales-admin : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
|
||||
|
||||
// =========================================================================
|
||||
// Configuration
|
||||
|
||||
var cfg struct {
|
||||
CMD string `envconfig:"CMD"`
|
||||
DB struct {
|
||||
DialTimeout time.Duration `default:"5s" envconfig:"DIAL_TIMEOUT"`
|
||||
Host string `default:"localhost:27017/gotraining" envconfig:"HOST"`
|
||||
}
|
||||
Auth struct {
|
||||
PrivateKeyFile string `default:"private.pem" envconfig:"PRIVATE_KEY_FILE"`
|
||||
}
|
||||
User struct {
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
}
|
||||
|
||||
if err := envconfig.Process("SALES", &cfg); err != nil {
|
||||
log.Fatalf("main : Parsing Config : %v", err)
|
||||
}
|
||||
|
||||
if err := flag.Process(&cfg); err != nil {
|
||||
if err != flag.ErrHelp {
|
||||
log.Fatalf("main : Parsing Command Line : %v", err)
|
||||
}
|
||||
return // We displayed help.
|
||||
}
|
||||
|
||||
var err error
|
||||
switch cfg.CMD {
|
||||
case "keygen":
|
||||
err = keygen(cfg.Auth.PrivateKeyFile)
|
||||
case "useradd":
|
||||
err = useradd(cfg.DB.Host, cfg.DB.DialTimeout, cfg.User.Email, cfg.User.Password)
|
||||
default:
|
||||
err = errors.New("Must provide --cmd keygen or --cmd useradd")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// keygen creates an x509 private key for signing auth tokens.
|
||||
func keygen(path string) error {
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "generating keys")
|
||||
}
|
||||
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating private file")
|
||||
}
|
||||
|
||||
block := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
}
|
||||
|
||||
if err := pem.Encode(file, &block); err != nil {
|
||||
return errors.Wrap(err, "encoding to private file")
|
||||
}
|
||||
|
||||
if err := file.Close(); err != nil {
|
||||
return errors.Wrap(err, "closing private file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func useradd(dbHost string, dbTimeout time.Duration, email, pass string) error {
|
||||
|
||||
dbConn, err := db.New(dbHost, dbTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbConn.Close()
|
||||
|
||||
if email == "" {
|
||||
return errors.New("Must provide --user_email")
|
||||
}
|
||||
if pass == "" {
|
||||
return errors.New("Must provide --user_password or set the env var SALES_USER_PASSWORD")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
newU := user.NewUser{
|
||||
Email: email,
|
||||
Password: pass,
|
||||
PasswordConfirm: pass,
|
||||
Roles: []string{auth.RoleAdmin, auth.RoleUser},
|
||||
}
|
||||
|
||||
usr, err := user.Create(ctx, dbConn, &newU, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("User created with id: %v\n", usr.ID.Hex())
|
||||
return nil
|
||||
}
|
||||
38
example-project/cmd/sales-api/handlers/check.go
Normal file
38
example-project/cmd/sales-api/handlers/check.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
// Check provides support for orchestration health checks.
|
||||
type Check struct {
|
||||
MasterDB *db.DB
|
||||
|
||||
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
|
||||
}
|
||||
|
||||
// Health validates the service is healthy and ready to accept requests.
|
||||
func (c *Check) Health(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.Check.Health")
|
||||
defer span.End()
|
||||
|
||||
dbConn := c.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
if err := dbConn.StatusCheck(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status := struct {
|
||||
Status string `json:"status"`
|
||||
}{
|
||||
Status: "ok",
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, status, http.StatusOK)
|
||||
}
|
||||
140
example-project/cmd/sales-api/handlers/product.go
Normal file
140
example-project/cmd/sales-api/handlers/product.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/product"
|
||||
"github.com/pkg/errors"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
// Product represents the Product API method handler set.
|
||||
type Product struct {
|
||||
MasterDB *db.DB
|
||||
|
||||
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
|
||||
}
|
||||
|
||||
// List returns all the existing products in the system.
|
||||
func (p *Product) List(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.Product.List")
|
||||
defer span.End()
|
||||
|
||||
dbConn := p.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
products, err := product.List(ctx, dbConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, products, http.StatusOK)
|
||||
}
|
||||
|
||||
// Retrieve returns the specified product from the system.
|
||||
func (p *Product) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.Product.Retrieve")
|
||||
defer span.End()
|
||||
|
||||
dbConn := p.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
prod, err := product.Retrieve(ctx, dbConn, params["id"])
|
||||
if err != nil {
|
||||
switch err {
|
||||
case product.ErrInvalidID:
|
||||
return web.NewRequestError(err, http.StatusBadRequest)
|
||||
case product.ErrNotFound:
|
||||
return web.NewRequestError(err, http.StatusNotFound)
|
||||
default:
|
||||
return errors.Wrapf(err, "ID: %s", params["id"])
|
||||
}
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, prod, http.StatusOK)
|
||||
}
|
||||
|
||||
// Create inserts a new product into the system.
|
||||
func (p *Product) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.Product.Create")
|
||||
defer span.End()
|
||||
|
||||
dbConn := p.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||
if !ok {
|
||||
return web.NewShutdownError("web value missing from context")
|
||||
}
|
||||
|
||||
var np product.NewProduct
|
||||
if err := web.Decode(r, &np); err != nil {
|
||||
return errors.Wrap(err, "")
|
||||
}
|
||||
|
||||
nUsr, err := product.Create(ctx, dbConn, &np, v.Now)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Product: %+v", &np)
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, nUsr, http.StatusCreated)
|
||||
}
|
||||
|
||||
// Update updates the specified product in the system.
|
||||
func (p *Product) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.Product.Update")
|
||||
defer span.End()
|
||||
|
||||
dbConn := p.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||
if !ok {
|
||||
return web.NewShutdownError("web value missing from context")
|
||||
}
|
||||
|
||||
var up product.UpdateProduct
|
||||
if err := web.Decode(r, &up); err != nil {
|
||||
return errors.Wrap(err, "")
|
||||
}
|
||||
|
||||
err := product.Update(ctx, dbConn, params["id"], up, v.Now)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case product.ErrInvalidID:
|
||||
return web.NewRequestError(err, http.StatusBadRequest)
|
||||
case product.ErrNotFound:
|
||||
return web.NewRequestError(err, http.StatusNotFound)
|
||||
default:
|
||||
return errors.Wrapf(err, "ID: %s Update: %+v", params["id"], up)
|
||||
}
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, nil, http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Delete removes the specified product from the system.
|
||||
func (p *Product) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.Product.Delete")
|
||||
defer span.End()
|
||||
|
||||
dbConn := p.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
err := product.Delete(ctx, dbConn, params["id"])
|
||||
if err != nil {
|
||||
switch err {
|
||||
case product.ErrInvalidID:
|
||||
return web.NewRequestError(err, http.StatusBadRequest)
|
||||
case product.ErrNotFound:
|
||||
return web.NewRequestError(err, http.StatusNotFound)
|
||||
default:
|
||||
return errors.Wrapf(err, "Id: %s", params["id"])
|
||||
}
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, nil, http.StatusNoContent)
|
||||
}
|
||||
51
example-project/cmd/sales-api/handlers/routes.go
Normal file
51
example-project/cmd/sales-api/handlers/routes.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||
)
|
||||
|
||||
// API returns a handler for a set of routes.
|
||||
func API(shutdown chan os.Signal, log *log.Logger, masterDB *db.DB, authenticator *auth.Authenticator) http.Handler {
|
||||
|
||||
// Construct the web.App which holds all routes as well as common Middleware.
|
||||
app := web.NewApp(shutdown, log, mid.Logger(log), mid.Errors(log), mid.Metrics(), mid.Panics())
|
||||
|
||||
// Register health check endpoint. This route is not authenticated.
|
||||
check := Check{
|
||||
MasterDB: masterDB,
|
||||
}
|
||||
app.Handle("GET", "/v1/health", check.Health)
|
||||
|
||||
// Register user management and authentication endpoints.
|
||||
u := User{
|
||||
MasterDB: masterDB,
|
||||
TokenGenerator: authenticator,
|
||||
}
|
||||
app.Handle("GET", "/v1/users", u.List, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/v1/users", u.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/v1/users/:id", u.Retrieve, mid.Authenticate(authenticator))
|
||||
app.Handle("PUT", "/v1/users/:id", u.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
|
||||
// This route is not authenticated
|
||||
app.Handle("GET", "/v1/users/token", u.Token)
|
||||
|
||||
// Register product and sale endpoints.
|
||||
p := Product{
|
||||
MasterDB: masterDB,
|
||||
}
|
||||
app.Handle("GET", "/v1/products", p.List, mid.Authenticate(authenticator))
|
||||
app.Handle("POST", "/v1/products", p.Create, mid.Authenticate(authenticator))
|
||||
app.Handle("GET", "/v1/products/:id", p.Retrieve, mid.Authenticate(authenticator))
|
||||
app.Handle("PUT", "/v1/products/:id", p.Update, mid.Authenticate(authenticator))
|
||||
app.Handle("DELETE", "/v1/products/:id", p.Delete, mid.Authenticate(authenticator))
|
||||
|
||||
return app
|
||||
}
|
||||
186
example-project/cmd/sales-api/handlers/user.go
Normal file
186
example-project/cmd/sales-api/handlers/user.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
||||
"github.com/pkg/errors"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
// User represents the User API method handler set.
|
||||
type User struct {
|
||||
MasterDB *db.DB
|
||||
TokenGenerator user.TokenGenerator
|
||||
|
||||
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
|
||||
}
|
||||
|
||||
// List returns all the existing users in the system.
|
||||
func (u *User) List(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.User.List")
|
||||
defer span.End()
|
||||
|
||||
dbConn := u.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
usrs, err := user.List(ctx, dbConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, usrs, http.StatusOK)
|
||||
}
|
||||
|
||||
// Retrieve returns the specified user from the system.
|
||||
func (u *User) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.User.Retrieve")
|
||||
defer span.End()
|
||||
|
||||
dbConn := u.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||
if !ok {
|
||||
return errors.New("claims missing from context")
|
||||
}
|
||||
|
||||
usr, err := user.Retrieve(ctx, claims, dbConn, params["id"])
|
||||
if err != nil {
|
||||
switch err {
|
||||
case user.ErrInvalidID:
|
||||
return web.NewRequestError(err, http.StatusBadRequest)
|
||||
case user.ErrNotFound:
|
||||
return web.NewRequestError(err, http.StatusNotFound)
|
||||
case user.ErrForbidden:
|
||||
return web.NewRequestError(err, http.StatusForbidden)
|
||||
default:
|
||||
return errors.Wrapf(err, "Id: %s", params["id"])
|
||||
}
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, usr, http.StatusOK)
|
||||
}
|
||||
|
||||
// Create inserts a new user into the system.
|
||||
func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.User.Create")
|
||||
defer span.End()
|
||||
|
||||
dbConn := u.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||
if !ok {
|
||||
return web.NewShutdownError("web value missing from context")
|
||||
}
|
||||
|
||||
var newU user.NewUser
|
||||
if err := web.Decode(r, &newU); err != nil {
|
||||
return errors.Wrap(err, "")
|
||||
}
|
||||
|
||||
usr, err := user.Create(ctx, dbConn, &newU, v.Now)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "User: %+v", &usr)
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, usr, http.StatusCreated)
|
||||
}
|
||||
|
||||
// Update updates the specified user in the system.
|
||||
func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.User.Update")
|
||||
defer span.End()
|
||||
|
||||
dbConn := u.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||
if !ok {
|
||||
return web.NewShutdownError("web value missing from context")
|
||||
}
|
||||
|
||||
var upd user.UpdateUser
|
||||
if err := web.Decode(r, &upd); err != nil {
|
||||
return errors.Wrap(err, "")
|
||||
}
|
||||
|
||||
err := user.Update(ctx, dbConn, params["id"], &upd, v.Now)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case user.ErrInvalidID:
|
||||
return web.NewRequestError(err, http.StatusBadRequest)
|
||||
case user.ErrNotFound:
|
||||
return web.NewRequestError(err, http.StatusNotFound)
|
||||
case user.ErrForbidden:
|
||||
return web.NewRequestError(err, http.StatusForbidden)
|
||||
default:
|
||||
return errors.Wrapf(err, "Id: %s User: %+v", params["id"], &upd)
|
||||
}
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, nil, http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Delete removes the specified user from the system.
|
||||
func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.User.Delete")
|
||||
defer span.End()
|
||||
|
||||
dbConn := u.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
err := user.Delete(ctx, dbConn, params["id"])
|
||||
if err != nil {
|
||||
switch err {
|
||||
case user.ErrInvalidID:
|
||||
return web.NewRequestError(err, http.StatusBadRequest)
|
||||
case user.ErrNotFound:
|
||||
return web.NewRequestError(err, http.StatusNotFound)
|
||||
case user.ErrForbidden:
|
||||
return web.NewRequestError(err, http.StatusForbidden)
|
||||
default:
|
||||
return errors.Wrapf(err, "Id: %s", params["id"])
|
||||
}
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, nil, http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Token handles a request to authenticate a user. It expects a request using
|
||||
// Basic Auth with a user's email and password. It responds with a JWT.
|
||||
func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.User.Token")
|
||||
defer span.End()
|
||||
|
||||
dbConn := u.MasterDB.Copy()
|
||||
defer dbConn.Close()
|
||||
|
||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||
if !ok {
|
||||
return web.NewShutdownError("web value missing from context")
|
||||
}
|
||||
|
||||
email, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
err := errors.New("must provide email and password in Basic auth")
|
||||
return web.NewRequestError(err, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
tkn, err := user.Authenticate(ctx, dbConn, u.TokenGenerator, v.Now, email, pass)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case user.ErrAuthenticationFailure:
|
||||
return web.NewRequestError(err, http.StatusUnauthorized)
|
||||
default:
|
||||
return errors.Wrap(err, "authenticating")
|
||||
}
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, tkn, http.StatusOK)
|
||||
}
|
||||
228
example-project/cmd/sales-api/main.go
Normal file
228
example-project/cmd/sales-api/main.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/sales-api/handlers"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
|
||||
itrace "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/trace"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
/*
|
||||
ZipKin: http://localhost:9411
|
||||
AddLoad: hey -m GET -c 10 -n 10000 "http://localhost:3000/v1/users"
|
||||
expvarmon -ports=":3001" -endpoint="/metrics" -vars="requests,goroutines,errors,mem:memstats.Alloc"
|
||||
*/
|
||||
|
||||
/*
|
||||
Need to figure out timeouts for http service.
|
||||
You might want to reset your DB_HOST env var during test tear down.
|
||||
Service should start even without a DB running yet.
|
||||
symbols in profiles: https://github.com/golang/go/issues/23376 / https://github.com/google/pprof/pull/366
|
||||
*/
|
||||
|
||||
// build is the git version of this program. It is set using build flags in the makefile.
|
||||
var build = "develop"
|
||||
|
||||
func main() {
|
||||
|
||||
// =========================================================================
|
||||
// Logging
|
||||
|
||||
log := log.New(os.Stdout, "SALES : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
|
||||
|
||||
// =========================================================================
|
||||
// Configuration
|
||||
|
||||
var cfg struct {
|
||||
Web struct {
|
||||
APIHost string `default:"0.0.0.0:3000" envconfig:"API_HOST"`
|
||||
DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"`
|
||||
ReadTimeout time.Duration `default:"5s" envconfig:"READ_TIMEOUT"`
|
||||
WriteTimeout time.Duration `default:"5s" envconfig:"WRITE_TIMEOUT"`
|
||||
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
|
||||
}
|
||||
DB struct {
|
||||
DialTimeout time.Duration `default:"5s" envconfig:"DIAL_TIMEOUT"`
|
||||
Host string `default:"mongo:27017/gotraining" envconfig:"HOST"`
|
||||
}
|
||||
Trace struct {
|
||||
Host string `default:"http://tracer:3002/v1/publish" envconfig:"HOST"`
|
||||
BatchSize int `default:"1000" envconfig:"BATCH_SIZE"`
|
||||
SendInterval time.Duration `default:"15s" envconfig:"SEND_INTERVAL"`
|
||||
SendTimeout time.Duration `default:"500ms" envconfig:"SEND_TIMEOUT"`
|
||||
}
|
||||
Auth struct {
|
||||
KeyID string `envconfig:"KEY_ID"`
|
||||
PrivateKeyFile string `default:"/app/private.pem" envconfig:"PRIVATE_KEY_FILE"`
|
||||
Algorithm string `default:"RS256" envconfig:"ALGORITHM"`
|
||||
}
|
||||
}
|
||||
|
||||
if err := envconfig.Process("SALES", &cfg); err != nil {
|
||||
log.Fatalf("main : Parsing Config : %v", err)
|
||||
}
|
||||
|
||||
if err := flag.Process(&cfg); err != nil {
|
||||
if err != flag.ErrHelp {
|
||||
log.Fatalf("main : Parsing Command Line : %v", err)
|
||||
}
|
||||
return // We displayed help.
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// App Starting
|
||||
|
||||
// Print the build version for our logs. Also expose it under /debug/vars.
|
||||
expvar.NewString("build").Set(build)
|
||||
log.Printf("main : Started : Application Initializing version %q", build)
|
||||
defer log.Println("main : Completed")
|
||||
|
||||
cfgJSON, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("main : Marshalling Config to JSON : %v", err)
|
||||
}
|
||||
|
||||
// TODO: Validate what is being written to the logs. We don't
|
||||
// want to leak credentials or anything that can be a security risk.
|
||||
log.Printf("main : Config : %v\n", string(cfgJSON))
|
||||
|
||||
// =========================================================================
|
||||
// Find auth keys
|
||||
|
||||
keyContents, err := ioutil.ReadFile(cfg.Auth.PrivateKeyFile)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Reading auth private key : %v", err)
|
||||
}
|
||||
|
||||
key, err := jwt.ParseRSAPrivateKeyFromPEM(keyContents)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Parsing auth private key : %v", err)
|
||||
}
|
||||
|
||||
publicKeyLookup := auth.NewSingleKeyFunc(cfg.Auth.KeyID, key.Public().(*rsa.PublicKey))
|
||||
|
||||
authenticator, err := auth.NewAuthenticator(key, cfg.Auth.KeyID, cfg.Auth.Algorithm, publicKeyLookup)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Constructing authenticator : %v", err)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Start Mongo
|
||||
|
||||
log.Println("main : Started : Initialize Mongo")
|
||||
masterDB, err := db.New(cfg.DB.Host, cfg.DB.DialTimeout)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Register DB : %v", err)
|
||||
}
|
||||
defer masterDB.Close()
|
||||
|
||||
// =========================================================================
|
||||
// Start Tracing Support
|
||||
|
||||
logger := func(format string, v ...interface{}) {
|
||||
log.Printf(format, v...)
|
||||
}
|
||||
|
||||
log.Printf("main : Tracing Started : %s", cfg.Trace.Host)
|
||||
exporter, err := itrace.NewExporter(logger, cfg.Trace.Host, cfg.Trace.BatchSize, cfg.Trace.SendInterval, cfg.Trace.SendTimeout)
|
||||
if err != nil {
|
||||
log.Fatalf("main : RegiTracingster : ERROR : %v", err)
|
||||
}
|
||||
defer func() {
|
||||
log.Printf("main : Tracing Stopping : %s", cfg.Trace.Host)
|
||||
batch, err := exporter.Close()
|
||||
if err != nil {
|
||||
log.Printf("main : Tracing Stopped : ERROR : Batch[%d] : %v", batch, err)
|
||||
} else {
|
||||
log.Printf("main : Tracing Stopped : Flushed Batch[%d]", batch)
|
||||
}
|
||||
}()
|
||||
|
||||
trace.RegisterExporter(exporter)
|
||||
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
|
||||
|
||||
// =========================================================================
|
||||
// Start Debug Service. Not concerned with shutting this down when the
|
||||
// application is being shutdown.
|
||||
//
|
||||
// /debug/vars - Added to the default mux by the expvars package.
|
||||
// /debug/pprof - Added to the default mux by the net/http/pprof package.
|
||||
go func() {
|
||||
log.Printf("main : Debug Listening %s", cfg.Web.DebugHost)
|
||||
log.Printf("main : Debug Listener closed : %v", http.ListenAndServe(cfg.Web.DebugHost, http.DefaultServeMux))
|
||||
}()
|
||||
|
||||
// =========================================================================
|
||||
// Start API Service
|
||||
|
||||
// Make a channel to listen for an interrupt or terminate signal from the OS.
|
||||
// Use a buffered channel because the signal package requires it.
|
||||
shutdown := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
api := http.Server{
|
||||
Addr: cfg.Web.APIHost,
|
||||
Handler: handlers.API(shutdown, log, masterDB, authenticator),
|
||||
ReadTimeout: cfg.Web.ReadTimeout,
|
||||
WriteTimeout: cfg.Web.WriteTimeout,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
|
||||
// Make a channel to listen for errors coming from the listener. Use a
|
||||
// buffered channel so the goroutine can exit if we don't collect this error.
|
||||
serverErrors := make(chan error, 1)
|
||||
|
||||
// Start the service listening for requests.
|
||||
go func() {
|
||||
log.Printf("main : API Listening %s", cfg.Web.APIHost)
|
||||
serverErrors <- api.ListenAndServe()
|
||||
}()
|
||||
|
||||
// =========================================================================
|
||||
// Shutdown
|
||||
|
||||
// Blocking main and waiting for shutdown.
|
||||
select {
|
||||
case err := <-serverErrors:
|
||||
log.Fatalf("main : Error starting server: %v", err)
|
||||
|
||||
case sig := <-shutdown:
|
||||
log.Printf("main : %v : Start shutdown..", sig)
|
||||
|
||||
// Create context for Shutdown call.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.Web.ShutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Asking listener to shutdown and load shed.
|
||||
err := api.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Printf("main : Graceful shutdown did not complete in %v : %v", cfg.Web.ShutdownTimeout, err)
|
||||
err = api.Close()
|
||||
}
|
||||
|
||||
// Log the status of this shutdown.
|
||||
switch {
|
||||
case sig == syscall.SIGSTOP:
|
||||
log.Fatal("main : Integrity issue caused shutdown")
|
||||
case err != nil:
|
||||
log.Fatalf("main : Could not stop server gracefully : %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
447
example-project/cmd/sales-api/tests/product_test.go
Normal file
447
example-project/cmd/sales-api/tests/product_test.go
Normal file
@@ -0,0 +1,447 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/product"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
// TestProducts is the entry point for the products
|
||||
func TestProducts(t *testing.T) {
|
||||
defer tests.Recover(t)
|
||||
|
||||
t.Run("getProducts200Empty", getProducts200Empty)
|
||||
t.Run("postProduct400", postProduct400)
|
||||
t.Run("postProduct401", postProduct401)
|
||||
t.Run("getProduct404", getProduct404)
|
||||
t.Run("getProduct400", getProduct400)
|
||||
t.Run("deleteProduct404", deleteProduct404)
|
||||
t.Run("putProduct404", putProduct404)
|
||||
t.Run("crudProducts", crudProduct)
|
||||
}
|
||||
|
||||
// getProducts200Empty validates an empty products list can be retrieved with the endpoint.
|
||||
func getProducts200Empty(t *testing.T) {
|
||||
r := httptest.NewRequest("GET", "/v1/products", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to fetch an empty list of products with the products endpoint.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen fetching an empty product list.")
|
||||
{
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success)
|
||||
|
||||
recv := w.Body.String()
|
||||
resp := `[]`
|
||||
if resp != recv {
|
||||
t.Log("Got :", recv)
|
||||
t.Log("Want:", resp)
|
||||
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// postProduct400 validates a product can't be created with the endpoint
|
||||
// unless a valid product document is submitted.
|
||||
func postProduct400(t *testing.T) {
|
||||
r := httptest.NewRequest("POST", "/v1/products", strings.NewReader(`{}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate a new product can't be created with an invalid document.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen using an incomplete product value.")
|
||||
{
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success)
|
||||
|
||||
// Inspect the response.
|
||||
var got web.ErrorResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
|
||||
t.Fatalf("\t%s\tShould be able to unmarshal the response to an error type : %v", tests.Failed, err)
|
||||
}
|
||||
t.Logf("\t%s\tShould be able to unmarshal the response to an error type.", tests.Success)
|
||||
|
||||
// Define what we want to see.
|
||||
want := web.ErrorResponse{
|
||||
Error: "field validation error",
|
||||
Fields: []web.FieldError{
|
||||
{Field: "name", Error: "name is a required field"},
|
||||
{Field: "cost", Error: "cost is a required field"},
|
||||
{Field: "quantity", Error: "quantity is a required field"},
|
||||
},
|
||||
}
|
||||
|
||||
// We can't rely on the order of the field errors so they have to be
|
||||
// sorted. Tell the cmp package how to sort them.
|
||||
sorter := cmpopts.SortSlices(func(a, b web.FieldError) bool {
|
||||
return a.Field < b.Field
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(want, got, sorter); diff != "" {
|
||||
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// postProduct401 validates a product can't be created with the endpoint
|
||||
// unless the user is authenticated
|
||||
func postProduct401(t *testing.T) {
|
||||
np := product.NewProduct{
|
||||
Name: "Comic Books",
|
||||
Cost: 25,
|
||||
Quantity: 60,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(&np)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("POST", "/v1/products", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Not setting an authorization header
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate a new product can't be created with an invalid document.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen using an incomplete product value.")
|
||||
{
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 401 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 401 for the response.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getProduct400 validates a product request for a malformed id.
|
||||
func getProduct400(t *testing.T) {
|
||||
id := "12345"
|
||||
|
||||
r := httptest.NewRequest("GET", "/v1/products/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate getting a product with a malformed id.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success)
|
||||
|
||||
recv := w.Body.String()
|
||||
resp := `{"error":"ID is not in its proper form"}`
|
||||
if resp != recv {
|
||||
t.Log("Got :", recv)
|
||||
t.Log("Want:", resp)
|
||||
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getProduct404 validates a product request for a product that does not exist with the endpoint.
|
||||
func getProduct404(t *testing.T) {
|
||||
id := bson.NewObjectId().Hex()
|
||||
|
||||
r := httptest.NewRequest("GET", "/v1/products/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate getting a product with an unknown id.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
|
||||
|
||||
recv := w.Body.String()
|
||||
resp := "Entity not found"
|
||||
if !strings.Contains(recv, resp) {
|
||||
t.Log("Got :", recv)
|
||||
t.Log("Want:", resp)
|
||||
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deleteProduct404 validates deleting a product that does not exist.
|
||||
func deleteProduct404(t *testing.T) {
|
||||
id := bson.NewObjectId().Hex()
|
||||
|
||||
r := httptest.NewRequest("DELETE", "/v1/products/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate deleting a product that does not exist.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
|
||||
|
||||
recv := w.Body.String()
|
||||
resp := "Entity not found"
|
||||
if !strings.Contains(recv, resp) {
|
||||
t.Log("Got :", recv)
|
||||
t.Log("Want:", resp)
|
||||
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// putProduct404 validates updating a product that does not exist.
|
||||
func putProduct404(t *testing.T) {
|
||||
up := product.UpdateProduct{
|
||||
Name: tests.StringPointer("Nonexistent"),
|
||||
}
|
||||
|
||||
id := bson.NewObjectId().Hex()
|
||||
|
||||
body, err := json.Marshal(&up)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("PUT", "/v1/products/"+id, bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate updating a product that does not exist.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
|
||||
|
||||
recv := w.Body.String()
|
||||
resp := "Entity not found"
|
||||
if !strings.Contains(recv, resp) {
|
||||
t.Log("Got :", recv)
|
||||
t.Log("Want:", resp)
|
||||
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// crudProduct performs a complete test of CRUD against the api.
|
||||
func crudProduct(t *testing.T) {
|
||||
p := postProduct201(t)
|
||||
defer deleteProduct204(t, p.ID.Hex())
|
||||
|
||||
getProduct200(t, p.ID.Hex())
|
||||
putProduct204(t, p.ID.Hex())
|
||||
}
|
||||
|
||||
// postProduct201 validates a product can be created with the endpoint.
|
||||
func postProduct201(t *testing.T) product.Product {
|
||||
np := product.NewProduct{
|
||||
Name: "Comic Books",
|
||||
Cost: 25,
|
||||
Quantity: 60,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(&np)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("POST", "/v1/products", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
// p is the value we will return.
|
||||
var p product.Product
|
||||
|
||||
t.Log("Given the need to create a new product with the products endpoint.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen using the declared product value.")
|
||||
{
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 201 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 201 for the response.", tests.Success)
|
||||
|
||||
if err := json.NewDecoder(w.Body).Decode(&p); err != nil {
|
||||
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
|
||||
}
|
||||
|
||||
// Define what we wanted to receive. We will just trust the generated
|
||||
// fields like ID and Dates so we copy p.
|
||||
want := p
|
||||
want.Name = "Comic Books"
|
||||
want.Cost = 25
|
||||
want.Quantity = 60
|
||||
|
||||
if diff := cmp.Diff(want, p); diff != "" {
|
||||
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// deleteProduct200 validates deleting a product that does exist.
|
||||
func deleteProduct204(t *testing.T, id string) {
|
||||
r := httptest.NewRequest("DELETE", "/v1/products/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate deleting a product that does exist.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getProduct200 validates a product request for an existing id.
|
||||
func getProduct200(t *testing.T, id string) {
|
||||
r := httptest.NewRequest("GET", "/v1/products/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate getting a product that exists.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success)
|
||||
|
||||
var p product.Product
|
||||
if err := json.NewDecoder(w.Body).Decode(&p); err != nil {
|
||||
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
|
||||
}
|
||||
|
||||
// Define what we wanted to receive. We will just trust the generated
|
||||
// fields like Dates so we copy p.
|
||||
want := p
|
||||
want.ID = bson.ObjectIdHex(id)
|
||||
want.Name = "Comic Books"
|
||||
want.Cost = 25
|
||||
want.Quantity = 60
|
||||
|
||||
if diff := cmp.Diff(want, p); diff != "" {
|
||||
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// putProduct204 validates updating a product that does exist.
|
||||
func putProduct204(t *testing.T, id string) {
|
||||
body := `{"name": "Graphic Novels", "cost": 100}`
|
||||
r := httptest.NewRequest("PUT", "/v1/products/"+id, strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to update a product with the products endpoint.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen using the modified product value.")
|
||||
{
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success)
|
||||
|
||||
r = httptest.NewRequest("GET", "/v1/products/"+id, nil)
|
||||
w = httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 200 for the retrieve : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 200 for the retrieve.", tests.Success)
|
||||
|
||||
var ru product.Product
|
||||
if err := json.NewDecoder(w.Body).Decode(&ru); err != nil {
|
||||
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
|
||||
}
|
||||
|
||||
if ru.Name != "Graphic Novels" {
|
||||
t.Fatalf("\t%s\tShould see an updated Name : got %q want %q", tests.Failed, ru.Name, "Graphic Novels")
|
||||
}
|
||||
t.Logf("\t%s\tShould see an updated Name.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
97
example-project/cmd/sales-api/tests/tests_test.go
Normal file
97
example-project/cmd/sales-api/tests/tests_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/sales-api/handlers"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
||||
)
|
||||
|
||||
var a http.Handler
|
||||
var test *tests.Test
|
||||
|
||||
// Information about the users we have created for testing.
|
||||
var adminAuthorization string
|
||||
var adminID string
|
||||
var userAuthorization string
|
||||
var userID string
|
||||
|
||||
// TestMain is the entry point for testing.
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(testMain(m))
|
||||
}
|
||||
|
||||
func testMain(m *testing.M) int {
|
||||
test = tests.New()
|
||||
defer test.TearDown()
|
||||
|
||||
// Create RSA keys to enable authentication in our service.
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
kid := "4754d86b-7a6d-4df5-9c65-224741361492"
|
||||
kf := auth.NewSingleKeyFunc(kid, key.Public().(*rsa.PublicKey))
|
||||
authenticator, err := auth.NewAuthenticator(key, kid, "RS256", kf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
shutdown := make(chan os.Signal, 1)
|
||||
a = handlers.API(shutdown, test.Log, test.MasterDB, authenticator)
|
||||
|
||||
// Create an admin user directly with our business logic. This creates an
|
||||
// initial user that we will use for admin validated endpoints.
|
||||
nu := user.NewUser{
|
||||
Email: "admin@ardanlabs.com",
|
||||
Name: "Admin User",
|
||||
Roles: []string{auth.RoleAdmin, auth.RoleUser},
|
||||
Password: "gophers",
|
||||
PasswordConfirm: "gophers",
|
||||
}
|
||||
|
||||
admin, err := user.Create(tests.Context(), test.MasterDB, &nu, time.Now())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
adminID = admin.ID.Hex()
|
||||
|
||||
tkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, time.Now(), nu.Email, nu.Password)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
adminAuthorization = "Bearer " + tkn.Token
|
||||
|
||||
// Create a regular user to use when calling regular validated endpoints.
|
||||
nu = user.NewUser{
|
||||
Email: "user@ardanlabs.com",
|
||||
Name: "Regular User",
|
||||
Roles: []string{auth.RoleUser},
|
||||
Password: "concurrency",
|
||||
PasswordConfirm: "concurrency",
|
||||
}
|
||||
|
||||
usr, err := user.Create(tests.Context(), test.MasterDB, &nu, time.Now())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
userID = usr.ID.Hex()
|
||||
|
||||
tkn, err = user.Authenticate(tests.Context(), test.MasterDB, authenticator, time.Now(), nu.Email, nu.Password)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
userAuthorization = "Bearer " + tkn.Token
|
||||
|
||||
return m.Run()
|
||||
}
|
||||
576
example-project/cmd/sales-api/tests/user_test.go
Normal file
576
example-project/cmd/sales-api/tests/user_test.go
Normal file
@@ -0,0 +1,576 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
// TestUsers is the entry point for testing user management functions.
|
||||
func TestUsers(t *testing.T) {
|
||||
defer tests.Recover(t)
|
||||
|
||||
t.Run("getToken401", getToken401)
|
||||
t.Run("getToken200", getToken200)
|
||||
t.Run("postUser400", postUser400)
|
||||
t.Run("postUser401", postUser401)
|
||||
t.Run("postUser403", postUser403)
|
||||
t.Run("getUser400", getUser400)
|
||||
t.Run("getUser403", getUser403)
|
||||
t.Run("getUser404", getUser404)
|
||||
t.Run("deleteUser404", deleteUser404)
|
||||
t.Run("putUser404", putUser404)
|
||||
t.Run("crudUsers", crudUser)
|
||||
}
|
||||
|
||||
// getToken401 ensures an unknown user can't generate a token.
|
||||
func getToken401(t *testing.T) {
|
||||
r := httptest.NewRequest("GET", "/v1/users/token", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.SetBasicAuth("unknown@example.com", "some-password")
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to deny tokens to unknown users.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen fetching a token with an unrecognized email.")
|
||||
{
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 401 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 401 for the response.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getToken200
|
||||
func getToken200(t *testing.T) {
|
||||
|
||||
r := httptest.NewRequest("GET", "/v1/users/token", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.SetBasicAuth("admin@ardanlabs.com", "gophers")
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to issues tokens to known users.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen fetching a token with valid credentials.")
|
||||
{
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success)
|
||||
|
||||
var got user.Token
|
||||
if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
|
||||
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
|
||||
}
|
||||
t.Logf("\t%s\tShould be able to unmarshal the response.", tests.Success)
|
||||
|
||||
// TODO(jlw) Should we ensure the token is valid?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// postUser400 validates a user can't be created with the endpoint
|
||||
// unless a valid user document is submitted.
|
||||
func postUser400(t *testing.T) {
|
||||
body, err := json.Marshal(&user.NewUser{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", adminAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate a new user can't be created with an invalid document.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen using an incomplete user value.")
|
||||
{
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success)
|
||||
|
||||
// Inspect the response.
|
||||
var got web.ErrorResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
|
||||
t.Fatalf("\t%s\tShould be able to unmarshal the response to an error type : %v", tests.Failed, err)
|
||||
}
|
||||
t.Logf("\t%s\tShould be able to unmarshal the response to an error type.", tests.Success)
|
||||
|
||||
// Define what we want to see.
|
||||
want := web.ErrorResponse{
|
||||
Error: "field validation error",
|
||||
Fields: []web.FieldError{
|
||||
{Field: "name", Error: "name is a required field"},
|
||||
{Field: "email", Error: "email is a required field"},
|
||||
{Field: "roles", Error: "roles is a required field"},
|
||||
{Field: "password", Error: "password is a required field"},
|
||||
},
|
||||
}
|
||||
|
||||
// We can't rely on the order of the field errors so they have to be
|
||||
// sorted. Tell the cmp package how to sort them.
|
||||
sorter := cmpopts.SortSlices(func(a, b web.FieldError) bool {
|
||||
return a.Field < b.Field
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(want, got, sorter); diff != "" {
|
||||
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// postUser401 validates a user can't be created unless the calling user is
|
||||
// authenticated.
|
||||
func postUser401(t *testing.T) {
|
||||
body, err := json.Marshal(&user.User{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate a new user can't be created with an invalid document.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen using an incomplete user value.")
|
||||
{
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 403 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 403 for the response.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// postUser403 validates a user can't be created unless the calling user is
|
||||
// an admin user. Regular users can't do this.
|
||||
func postUser403(t *testing.T) {
|
||||
body, err := json.Marshal(&user.User{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Not setting the Authorization header
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate a new user can't be created with an invalid document.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen using an incomplete user value.")
|
||||
{
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 401 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 401 for the response.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getUser400 validates a user request for a malformed userid.
|
||||
func getUser400(t *testing.T) {
|
||||
id := "12345"
|
||||
|
||||
r := httptest.NewRequest("GET", "/v1/users/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", adminAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate getting a user with a malformed userid.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success)
|
||||
|
||||
recv := w.Body.String()
|
||||
resp := `{"error":"ID is not in its proper form"}`
|
||||
if resp != recv {
|
||||
t.Log("Got :", recv)
|
||||
t.Log("Want:", resp)
|
||||
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getUser403 validates a regular user can't fetch anyone but themselves
|
||||
func getUser403(t *testing.T) {
|
||||
t.Log("Given the need to validate regular users can't fetch other users.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen fetching the admin user as a regular user.")
|
||||
{
|
||||
r := httptest.NewRequest("GET", "/v1/users/"+adminID, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 403 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 403 for the response.", tests.Success)
|
||||
|
||||
recv := w.Body.String()
|
||||
resp := `{"error":"Attempted action is not allowed"}`
|
||||
if resp != recv {
|
||||
t.Log("Got :", recv)
|
||||
t.Log("Want:", resp)
|
||||
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
|
||||
t.Logf("\tTest 1:\tWhen fetching the user as a themselves.")
|
||||
{
|
||||
|
||||
r := httptest.NewRequest("GET", "/v1/users/"+userID, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getUser404 validates a user request for a user that does not exist with the endpoint.
|
||||
func getUser404(t *testing.T) {
|
||||
id := bson.NewObjectId().Hex()
|
||||
|
||||
r := httptest.NewRequest("GET", "/v1/users/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", adminAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate getting a user with an unknown id.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
|
||||
|
||||
recv := w.Body.String()
|
||||
resp := "Entity not found"
|
||||
if !strings.Contains(recv, resp) {
|
||||
t.Log("Got :", recv)
|
||||
t.Log("Want:", resp)
|
||||
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deleteUser404 validates deleting a user that does not exist.
|
||||
func deleteUser404(t *testing.T) {
|
||||
id := bson.NewObjectId().Hex()
|
||||
|
||||
r := httptest.NewRequest("DELETE", "/v1/users/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", adminAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate deleting a user that does not exist.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
|
||||
|
||||
recv := w.Body.String()
|
||||
resp := "Entity not found"
|
||||
if !strings.Contains(recv, resp) {
|
||||
t.Log("Got :", recv)
|
||||
t.Log("Want:", resp)
|
||||
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// putUser404 validates updating a user that does not exist.
|
||||
func putUser404(t *testing.T) {
|
||||
u := user.UpdateUser{
|
||||
Name: tests.StringPointer("Doesn't Exist"),
|
||||
}
|
||||
|
||||
id := bson.NewObjectId().Hex()
|
||||
|
||||
body, err := json.Marshal(&u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("PUT", "/v1/users/"+id, bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", adminAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate updating a user that does not exist.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
|
||||
|
||||
recv := w.Body.String()
|
||||
resp := "Entity not found"
|
||||
if !strings.Contains(recv, resp) {
|
||||
t.Log("Got :", recv)
|
||||
t.Log("Want:", resp)
|
||||
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// crudUser performs a complete test of CRUD against the api.
|
||||
func crudUser(t *testing.T) {
|
||||
nu := postUser201(t)
|
||||
defer deleteUser204(t, nu.ID.Hex())
|
||||
|
||||
getUser200(t, nu.ID.Hex())
|
||||
putUser204(t, nu.ID.Hex())
|
||||
putUser403(t, nu.ID.Hex())
|
||||
}
|
||||
|
||||
// postUser201 validates a user can be created with the endpoint.
|
||||
func postUser201(t *testing.T) user.User {
|
||||
nu := user.NewUser{
|
||||
Name: "Bill Kennedy",
|
||||
Email: "bill@ardanlabs.com",
|
||||
Roles: []string{auth.RoleAdmin},
|
||||
Password: "gophers",
|
||||
PasswordConfirm: "gophers",
|
||||
}
|
||||
|
||||
body, err := json.Marshal(&nu)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", adminAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
// u is the value we will return.
|
||||
var u user.User
|
||||
|
||||
t.Log("Given the need to create a new user with the users endpoint.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen using the declared user value.")
|
||||
{
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 201 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 201 for the response.", tests.Success)
|
||||
|
||||
if err := json.NewDecoder(w.Body).Decode(&u); err != nil {
|
||||
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
|
||||
}
|
||||
|
||||
// Define what we wanted to receive. We will just trust the generated
|
||||
// fields like ID and Dates so we copy u.
|
||||
want := u
|
||||
want.Name = "Bill Kennedy"
|
||||
want.Email = "bill@ardanlabs.com"
|
||||
want.Roles = []string{auth.RoleAdmin}
|
||||
|
||||
if diff := cmp.Diff(want, u); diff != "" {
|
||||
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
// deleteUser200 validates deleting a user that does exist.
|
||||
func deleteUser204(t *testing.T, id string) {
|
||||
r := httptest.NewRequest("DELETE", "/v1/users/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", adminAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate deleting a user that does exist.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getUser200 validates a user request for an existing userid.
|
||||
func getUser200(t *testing.T, id string) {
|
||||
r := httptest.NewRequest("GET", "/v1/users/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", adminAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to validate getting a user that exsits.")
|
||||
{
|
||||
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
|
||||
{
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success)
|
||||
|
||||
var u user.User
|
||||
if err := json.NewDecoder(w.Body).Decode(&u); err != nil {
|
||||
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
|
||||
}
|
||||
|
||||
// Define what we wanted to receive. We will just trust the generated
|
||||
// fields like Dates so we copy p.
|
||||
want := u
|
||||
want.ID = bson.ObjectIdHex(id)
|
||||
want.Name = "Bill Kennedy"
|
||||
want.Email = "bill@ardanlabs.com"
|
||||
want.Roles = []string{auth.RoleAdmin}
|
||||
|
||||
if diff := cmp.Diff(want, u); diff != "" {
|
||||
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
|
||||
}
|
||||
t.Logf("\t%s\tShould get the expected result.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// putUser204 validates updating a user that does exist.
|
||||
func putUser204(t *testing.T, id string) {
|
||||
body := `{"name": "Jacob Walker"}`
|
||||
|
||||
r := httptest.NewRequest("PUT", "/v1/users/"+id, strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", adminAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to update a user with the users endpoint.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen using the modified user value.")
|
||||
{
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success)
|
||||
|
||||
r = httptest.NewRequest("GET", "/v1/users/"+id, nil)
|
||||
w = httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", adminAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 200 for the retrieve : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 200 for the retrieve.", tests.Success)
|
||||
|
||||
var ru user.User
|
||||
if err := json.NewDecoder(w.Body).Decode(&ru); err != nil {
|
||||
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
|
||||
}
|
||||
|
||||
if ru.Name != "Jacob Walker" {
|
||||
t.Fatalf("\t%s\tShould see an updated Name : got %q want %q", tests.Failed, ru.Name, "Jacob Walker")
|
||||
}
|
||||
t.Logf("\t%s\tShould see an updated Name.", tests.Success)
|
||||
|
||||
if ru.Email != "bill@ardanlabs.com" {
|
||||
t.Fatalf("\t%s\tShould not affect other fields like Email : got %q want %q", tests.Failed, ru.Email, "bill@ardanlabs.com")
|
||||
}
|
||||
t.Logf("\t%s\tShould not affect other fields like Email.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// putUser403 validates that a user can't modify users unless they are an admin.
|
||||
func putUser403(t *testing.T, id string) {
|
||||
body := `{"name": "Anna Walker"}`
|
||||
|
||||
r := httptest.NewRequest("PUT", "/v1/users/"+id, strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
t.Log("Given the need to update a user with the users endpoint.")
|
||||
{
|
||||
t.Log("\tTest 0:\tWhen a non-admin user makes a request")
|
||||
{
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("\t%s\tShould receive a status code of 403 for the response : %v", tests.Failed, w.Code)
|
||||
}
|
||||
t.Logf("\t%s\tShould receive a status code of 403 for the response.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
73
example-project/cmd/sidecar/metrics/collector/expvar.go
Normal file
73
example-project/cmd/sidecar/metrics/collector/expvar.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Expvar provides the ability to receive metrics
|
||||
// from internal services using expvar.
|
||||
type Expvar struct {
|
||||
host string
|
||||
tr *http.Transport
|
||||
client http.Client
|
||||
}
|
||||
|
||||
// New creates a Expvar for collection metrics.
|
||||
func New(host string) (*Expvar, error) {
|
||||
tr := http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 2,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
exp := Expvar{
|
||||
host: host,
|
||||
tr: &tr,
|
||||
client: http.Client{
|
||||
Transport: &tr,
|
||||
Timeout: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
return &exp, nil
|
||||
}
|
||||
|
||||
func (exp *Expvar) Collect() (map[string]interface{}, error) {
|
||||
req, err := http.NewRequest("GET", exp.host, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := exp.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.New(string(msg))
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
109
example-project/cmd/sidecar/metrics/main.go
Normal file
109
example-project/cmd/sidecar/metrics/main.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/sidecar/metrics/collector"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/sidecar/metrics/publisher"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/sidecar/metrics/publisher/expvar"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// =========================================================================
|
||||
// Logging
|
||||
|
||||
log := log.New(os.Stdout, "TRACER : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
|
||||
defer log.Println("main : Completed")
|
||||
|
||||
// =========================================================================
|
||||
// Configuration
|
||||
|
||||
var cfg struct {
|
||||
Web struct {
|
||||
DebugHost string `default:"0.0.0.0:4001" envconfig:"DEBUG_HOST"`
|
||||
ReadTimeout time.Duration `default:"5s" envconfig:"READ_TIMEOUT"`
|
||||
WriteTimeout time.Duration `default:"5s" envconfig:"WRITE_TIMEOUT"`
|
||||
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
|
||||
}
|
||||
Expvar struct {
|
||||
Host string `default:"0.0.0.0:3001" envconfig:"HOST"`
|
||||
Route string `default:"/metrics" envconfig:"ROUTE"`
|
||||
ReadTimeout time.Duration `default:"5s" envconfig:"READ_TIMEOUT"`
|
||||
WriteTimeout time.Duration `default:"5s" envconfig:"WRITE_TIMEOUT"`
|
||||
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
|
||||
}
|
||||
Collect struct {
|
||||
From string `default:"http://sales-api:4000/debug/vars" envconfig:"FROM"`
|
||||
}
|
||||
Publish struct {
|
||||
To string `default:"console" envconfig:"TO"`
|
||||
Interval time.Duration `default:"5s" envconfig:"INTERVAL"`
|
||||
}
|
||||
}
|
||||
|
||||
if err := envconfig.Process("METRICS", &cfg); err != nil {
|
||||
log.Fatalf("main : Parsing Config : %v", err)
|
||||
}
|
||||
|
||||
cfgJSON, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("main : Marshalling Config to JSON : %v", err)
|
||||
}
|
||||
log.Printf("config : %v\n", string(cfgJSON))
|
||||
|
||||
// =========================================================================
|
||||
// Start Debug Service. Not concerned with shutting this down when the
|
||||
// application is being shutdown.
|
||||
//
|
||||
// /debug/pprof - Added to the default mux by the net/http/pprof package.
|
||||
go func() {
|
||||
log.Printf("main : Debug Listening %s", cfg.Web.DebugHost)
|
||||
log.Printf("main : Debug Listener closed : %v", http.ListenAndServe(cfg.Web.DebugHost, http.DefaultServeMux))
|
||||
}()
|
||||
|
||||
// =========================================================================
|
||||
// Start expvar Service
|
||||
|
||||
exp := expvar.New(log, cfg.Expvar.Host, cfg.Expvar.Route, cfg.Expvar.ReadTimeout, cfg.Expvar.WriteTimeout)
|
||||
defer exp.Stop(cfg.Expvar.ShutdownTimeout)
|
||||
|
||||
// =========================================================================
|
||||
// Start collectors and publishers
|
||||
|
||||
// Initialize to allow for the collection of metrics.
|
||||
collector, err := collector.New(cfg.Collect.From)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Starting collector : %v", err)
|
||||
}
|
||||
|
||||
// Create a stdout publisher.
|
||||
// TODO: Respect the cfg.publish.to config option.
|
||||
stdout := publisher.NewStdout(log)
|
||||
|
||||
// Start the publisher to collect/publish metrics.
|
||||
publish, err := publisher.New(log, collector, cfg.Publish.Interval, exp.Publish, stdout.Publish)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Starting publisher : %v", err)
|
||||
}
|
||||
defer publish.Stop()
|
||||
|
||||
// =========================================================================
|
||||
// Shutdown
|
||||
|
||||
// Make a channel to listen for an interrupt or terminate signal from the OS.
|
||||
// Use a buffered channel because the signal package requires it.
|
||||
shutdown := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
|
||||
<-shutdown
|
||||
|
||||
log.Println("main : Start shutdown...")
|
||||
}
|
||||
164
example-project/cmd/sidecar/metrics/publisher/datadog/datadog.go
Normal file
164
example-project/cmd/sidecar/metrics/publisher/datadog/datadog.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package datadog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Datadog provides the ability to publish metrics to Datadog.
|
||||
type Datadog struct {
|
||||
log *log.Logger
|
||||
apiKey string
|
||||
host string
|
||||
tr *http.Transport
|
||||
client http.Client
|
||||
}
|
||||
|
||||
// New initializes Datadog access for publishing metrics.
|
||||
func New(log *log.Logger, apiKey string, host string) *Datadog {
|
||||
tr := http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 2,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
d := Datadog{
|
||||
log: log,
|
||||
apiKey: apiKey,
|
||||
host: host,
|
||||
tr: &tr,
|
||||
client: http.Client{
|
||||
Transport: &tr,
|
||||
Timeout: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
return &d
|
||||
}
|
||||
|
||||
// Publish handles the processing of metrics for deliver
|
||||
// to the DataDog.
|
||||
func (d *Datadog) Publish(data map[string]interface{}) {
|
||||
doc, err := marshalDatadog(d.log, data)
|
||||
if err != nil {
|
||||
d.log.Println("datadog.publish :", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sendDatadog(d, doc); err != nil {
|
||||
d.log.Println("datadog.publish :", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("datadog.publish : published :", string(doc))
|
||||
}
|
||||
|
||||
// marshalDatadog converts the data map to datadog JSON document.
|
||||
func marshalDatadog(log *log.Logger, data map[string]interface{}) ([]byte, error) {
|
||||
/*
|
||||
{ "series" : [
|
||||
{
|
||||
"metric":"test.metric",
|
||||
"points": [
|
||||
[
|
||||
$currenttime,
|
||||
20
|
||||
]
|
||||
],
|
||||
"type":"gauge",
|
||||
"host":"test.example.com",
|
||||
"tags": [
|
||||
"environment:test"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
// Extract the base keys/values.
|
||||
mType := "gauge"
|
||||
host, ok := data["host"].(string)
|
||||
if !ok {
|
||||
host = "unknown"
|
||||
}
|
||||
env := "dev"
|
||||
if host != "localhost" {
|
||||
env = "prod"
|
||||
}
|
||||
envTag := "environment:" + env
|
||||
|
||||
// Define the Datadog data format.
|
||||
type series struct {
|
||||
Metric string `json:"metric"`
|
||||
Points [][]interface{} `json:"points"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// Populate the data into the data structure.
|
||||
var doc struct {
|
||||
Series []series `json:"series"`
|
||||
}
|
||||
for key, value := range data {
|
||||
switch value.(type) {
|
||||
case int, float64:
|
||||
doc.Series = append(doc.Series, series{
|
||||
Metric: env + "." + key,
|
||||
Points: [][]interface{}{{"$currenttime", value}},
|
||||
Type: mType,
|
||||
Host: host,
|
||||
Tags: []string{envTag},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the data into JSON.
|
||||
out, err := json.MarshalIndent(doc, "", " ")
|
||||
if err != nil {
|
||||
log.Println("datadog.publish : marshaling :", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// sendDatadog sends data to the datadog servers.
|
||||
func sendDatadog(d *Datadog, data []byte) error {
|
||||
url := fmt.Sprintf("%s?api_key=%s", d.host, d.apiKey)
|
||||
b := bytes.NewBuffer(data)
|
||||
|
||||
r, err := http.NewRequest("POST", url, b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := d.client.Do(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
out, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("status[%d] : %s", resp.StatusCode, out)
|
||||
}
|
||||
return fmt.Errorf("status[%d]", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package expvar
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dimfeld/httptreemux"
|
||||
)
|
||||
|
||||
// Expvar provide our basic publishing.
|
||||
type Expvar struct {
|
||||
log *log.Logger
|
||||
server http.Server
|
||||
data map[string]interface{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// New starts a service for consuming the raw expvar stats.
|
||||
func New(log *log.Logger, host string, route string, readTimeout, writeTimeout time.Duration) *Expvar {
|
||||
mux := httptreemux.New()
|
||||
exp := Expvar{
|
||||
log: log,
|
||||
server: http.Server{
|
||||
Addr: host,
|
||||
Handler: mux,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
},
|
||||
}
|
||||
|
||||
mux.Handle("GET", route, exp.handler)
|
||||
|
||||
go func() {
|
||||
log.Println("expvar : API Listening", host)
|
||||
if err := exp.server.ListenAndServe(); err != nil {
|
||||
log.Println("expvar : ERROR :", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return &exp
|
||||
}
|
||||
|
||||
// Stop shuts down the service.
|
||||
func (exp *Expvar) Stop(shutdownTimeout time.Duration) {
|
||||
exp.log.Println("expvar : Start shutdown...")
|
||||
defer exp.log.Println("expvar : Completed")
|
||||
|
||||
// Create context for Shutdown call.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Asking listener to shutdown and load shed.
|
||||
if err := exp.server.Shutdown(ctx); err != nil {
|
||||
exp.log.Printf("expvar : Graceful shutdown did not complete in %v : %v", shutdownTimeout, err)
|
||||
if err := exp.server.Close(); err != nil {
|
||||
exp.log.Fatalf("expvar : Could not stop http server: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Publish is called by the publisher goroutine and saves the raw stats.
|
||||
func (exp *Expvar) Publish(data map[string]interface{}) {
|
||||
exp.mu.Lock()
|
||||
{
|
||||
exp.data = data
|
||||
}
|
||||
exp.mu.Unlock()
|
||||
}
|
||||
|
||||
// handler is what consumers call to get the raw stats.
|
||||
func (exp *Expvar) handler(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
var data map[string]interface{}
|
||||
exp.mu.Lock()
|
||||
{
|
||||
data = exp.data
|
||||
}
|
||||
exp.mu.Unlock()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
exp.log.Println("expvar : ERROR :", err)
|
||||
}
|
||||
|
||||
log.Printf("expvar : (%d) : %s %s -> %s",
|
||||
http.StatusOK,
|
||||
r.Method, r.URL.Path,
|
||||
r.RemoteAddr,
|
||||
)
|
||||
}
|
||||
128
example-project/cmd/sidecar/metrics/publisher/publisher.go
Normal file
128
example-project/cmd/sidecar/metrics/publisher/publisher.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package publisher
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Set of possible publisher types.
|
||||
const (
|
||||
TypeStdout = "stdout"
|
||||
TypeDatadog = "datadog"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
|
||||
// Collector defines a contract a collector must support
|
||||
// so a consumer can retrieve metrics.
|
||||
type Collector interface {
|
||||
Collect() (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
// Publisher defines a handler function that will be called
|
||||
// on each interval.
|
||||
type Publisher func(map[string]interface{})
|
||||
|
||||
// Publish provides the ability to receive metrics
|
||||
// on an interval.
|
||||
type Publish struct {
|
||||
log *log.Logger
|
||||
collector Collector
|
||||
publisher []Publisher
|
||||
wg sync.WaitGroup
|
||||
timer *time.Timer
|
||||
shutdown chan struct{}
|
||||
}
|
||||
|
||||
// New creates a Publish for consuming and publishing metrics.
|
||||
func New(log *log.Logger, collector Collector, interval time.Duration, publisher ...Publisher) (*Publish, error) {
|
||||
p := Publish{
|
||||
log: log,
|
||||
collector: collector,
|
||||
publisher: publisher,
|
||||
timer: time.NewTimer(interval),
|
||||
shutdown: make(chan struct{}),
|
||||
}
|
||||
|
||||
p.wg.Add(1)
|
||||
go func() {
|
||||
defer p.wg.Done()
|
||||
for {
|
||||
p.timer.Reset(interval)
|
||||
select {
|
||||
case <-p.timer.C:
|
||||
p.update()
|
||||
case <-p.shutdown:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// Stop is used to shutdown the goroutine collecting metrics.
|
||||
func (p *Publish) Stop() {
|
||||
close(p.shutdown)
|
||||
p.wg.Wait()
|
||||
}
|
||||
|
||||
// update pulls the metrics and publishes them to the specified system.
|
||||
func (p *Publish) update() {
|
||||
data, err := p.collector.Collect()
|
||||
if err != nil {
|
||||
p.log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pub := range p.publisher {
|
||||
pub(data)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
// Stdout provide our basic publishing.
|
||||
type Stdout struct {
|
||||
log *log.Logger
|
||||
}
|
||||
|
||||
// NewStdout initializes stdout for publishing metrics.
|
||||
func NewStdout(log *log.Logger) *Stdout {
|
||||
return &Stdout{log}
|
||||
}
|
||||
|
||||
// Publish publishers for writing to stdout.
|
||||
func (s *Stdout) Publish(data map[string]interface{}) {
|
||||
rawJSON, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
s.log.Println("Stdout : Marshal ERROR :", err)
|
||||
return
|
||||
}
|
||||
|
||||
var d map[string]interface{}
|
||||
if err := json.Unmarshal(rawJSON, &d); err != nil {
|
||||
s.log.Println("Stdout : Unmarshal ERROR :", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add heap value into the data set.
|
||||
memStats, ok := (d["memstats"]).(map[string]interface{})
|
||||
if ok {
|
||||
d["heap"] = memStats["Alloc"]
|
||||
}
|
||||
|
||||
// Remove unnecessary keys.
|
||||
delete(d, "memstats")
|
||||
delete(d, "cmdline")
|
||||
|
||||
out, err := json.MarshalIndent(d, "", " ")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.log.Println("Stdout :\n", string(out))
|
||||
}
|
||||
23
example-project/cmd/sidecar/tracer/handlers/health.go
Normal file
23
example-project/cmd/sidecar/tracer/handlers/health.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||
)
|
||||
|
||||
// Health provides support for orchestration health checks.
|
||||
type Health struct{}
|
||||
|
||||
// Check validates the service is ready and healthy to accept requests.
|
||||
func (h *Health) Check(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
status := struct {
|
||||
Status string `json:"status"`
|
||||
}{
|
||||
Status: "ok",
|
||||
}
|
||||
|
||||
web.Respond(ctx, w, status, http.StatusOK)
|
||||
return nil
|
||||
}
|
||||
25
example-project/cmd/sidecar/tracer/handlers/routes.go
Normal file
25
example-project/cmd/sidecar/tracer/handlers/routes.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||
)
|
||||
|
||||
// API returns a handler for a set of routes.
|
||||
func API(shutdown chan os.Signal, log *log.Logger, zipkinHost string, apiHost string) http.Handler {
|
||||
|
||||
app := web.NewApp(shutdown, log, mid.Logger(log), mid.Errors(log))
|
||||
|
||||
z := NewZipkin(zipkinHost, apiHost, time.Second)
|
||||
app.Handle("POST", "/v1/publish", z.Publish)
|
||||
|
||||
h := Health{}
|
||||
app.Handle("GET", "/v1/health", h.Check)
|
||||
|
||||
return app
|
||||
}
|
||||
326
example-project/cmd/sidecar/tracer/handlers/zipkin.go
Normal file
326
example-project/cmd/sidecar/tracer/handlers/zipkin.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||
"github.com/openzipkin/zipkin-go/model"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
// Zipkin represents the API to collect span data and send to zipkin.
|
||||
type Zipkin struct {
|
||||
zipkinHost string // IP:port of the zipkin service.
|
||||
localHost string // IP:port of the sidecare consuming the trace data.
|
||||
sendTimeout time.Duration // Time to wait for the sidecar to respond on send.
|
||||
client http.Client // Provides APIs for performing the http send.
|
||||
}
|
||||
|
||||
// NewZipkin provides support for publishing traces to zipkin.
|
||||
func NewZipkin(zipkinHost string, localHost string, sendTimeout time.Duration) *Zipkin {
|
||||
tr := http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 2,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
z := Zipkin{
|
||||
zipkinHost: zipkinHost,
|
||||
localHost: localHost,
|
||||
sendTimeout: sendTimeout,
|
||||
client: http.Client{
|
||||
Transport: &tr,
|
||||
},
|
||||
}
|
||||
|
||||
return &z
|
||||
}
|
||||
|
||||
// Publish takes a batch and publishes that to a host system.
|
||||
func (z *Zipkin) Publish(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
var sd []trace.SpanData
|
||||
if err := json.NewDecoder(r.Body).Decode(&sd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := z.send(sd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
web.Respond(ctx, w, nil, http.StatusNoContent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// send uses HTTP to send the data to the tracing sidecar for processing.
|
||||
func (z *Zipkin) send(sendBatch []trace.SpanData) error {
|
||||
le, err := newEndpoint("sales-api", z.localHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sm := convertForZipkin(sendBatch, le)
|
||||
data, err := json.Marshal(sm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", z.zipkinHost, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(req.Context(), z.sendTimeout)
|
||||
defer cancel()
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
ch := make(chan error)
|
||||
go func() {
|
||||
resp, err := z.client.Do(req)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
ch <- fmt.Errorf("error on call : status[%s]", resp.Status)
|
||||
return
|
||||
}
|
||||
ch <- fmt.Errorf("error on call : status[%s] : %s", resp.Status, string(data))
|
||||
return
|
||||
}
|
||||
|
||||
ch <- nil
|
||||
}()
|
||||
|
||||
return <-ch
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
statusCodeTagKey = "error"
|
||||
statusDescriptionTagKey = "opencensus.status_description"
|
||||
)
|
||||
|
||||
var (
|
||||
sampledTrue = true
|
||||
canonicalCodes = [...]string{
|
||||
"OK",
|
||||
"CANCELLED",
|
||||
"UNKNOWN",
|
||||
"INVALID_ARGUMENT",
|
||||
"DEADLINE_EXCEEDED",
|
||||
"NOT_FOUND",
|
||||
"ALREADY_EXISTS",
|
||||
"PERMISSION_DENIED",
|
||||
"RESOURCE_EXHAUSTED",
|
||||
"FAILED_PRECONDITION",
|
||||
"ABORTED",
|
||||
"OUT_OF_RANGE",
|
||||
"UNIMPLEMENTED",
|
||||
"INTERNAL",
|
||||
"UNAVAILABLE",
|
||||
"DATA_LOSS",
|
||||
"UNAUTHENTICATED",
|
||||
}
|
||||
)
|
||||
|
||||
func convertForZipkin(spanData []trace.SpanData, localEndpoint *model.Endpoint) []model.SpanModel {
|
||||
sm := make([]model.SpanModel, len(spanData))
|
||||
for i := range spanData {
|
||||
sm[i] = zipkinSpan(&spanData[i], localEndpoint)
|
||||
}
|
||||
return sm
|
||||
}
|
||||
|
||||
func newEndpoint(serviceName string, hostPort string) (*model.Endpoint, error) {
|
||||
e := &model.Endpoint{
|
||||
ServiceName: serviceName,
|
||||
}
|
||||
|
||||
if hostPort == "" || hostPort == ":0" {
|
||||
if serviceName == "" {
|
||||
// if all properties are empty we should not have an Endpoint object.
|
||||
return nil, nil
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
if strings.IndexByte(hostPort, ':') < 0 {
|
||||
hostPort += ":0"
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(hostPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.Port = uint16(p)
|
||||
|
||||
addrs, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range addrs {
|
||||
addr := addrs[i].To4()
|
||||
if addr == nil {
|
||||
// IPv6 - 16 bytes
|
||||
if e.IPv6 == nil {
|
||||
e.IPv6 = addrs[i].To16()
|
||||
}
|
||||
} else {
|
||||
// IPv4 - 4 bytes
|
||||
if e.IPv4 == nil {
|
||||
e.IPv4 = addr
|
||||
}
|
||||
}
|
||||
if e.IPv4 != nil && e.IPv6 != nil {
|
||||
// Both IPv4 & IPv6 have been set, done...
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// default to 0 filled 4 byte array for IPv4 if IPv6 only host was found
|
||||
if e.IPv4 == nil {
|
||||
e.IPv4 = make([]byte, 4)
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func canonicalCodeString(code int32) string {
|
||||
if code < 0 || int(code) >= len(canonicalCodes) {
|
||||
return "error code " + strconv.FormatInt(int64(code), 10)
|
||||
}
|
||||
return canonicalCodes[code]
|
||||
}
|
||||
|
||||
func convertTraceID(t trace.TraceID) model.TraceID {
|
||||
return model.TraceID{
|
||||
High: binary.BigEndian.Uint64(t[:8]),
|
||||
Low: binary.BigEndian.Uint64(t[8:]),
|
||||
}
|
||||
}
|
||||
|
||||
func convertSpanID(s trace.SpanID) model.ID {
|
||||
return model.ID(binary.BigEndian.Uint64(s[:]))
|
||||
}
|
||||
|
||||
func spanKind(s *trace.SpanData) model.Kind {
|
||||
switch s.SpanKind {
|
||||
case trace.SpanKindClient:
|
||||
return model.Client
|
||||
case trace.SpanKindServer:
|
||||
return model.Server
|
||||
}
|
||||
return model.Undetermined
|
||||
}
|
||||
|
||||
func zipkinSpan(s *trace.SpanData, localEndpoint *model.Endpoint) model.SpanModel {
|
||||
sc := s.SpanContext
|
||||
z := model.SpanModel{
|
||||
SpanContext: model.SpanContext{
|
||||
TraceID: convertTraceID(sc.TraceID),
|
||||
ID: convertSpanID(sc.SpanID),
|
||||
Sampled: &sampledTrue,
|
||||
},
|
||||
Kind: spanKind(s),
|
||||
Name: s.Name,
|
||||
Timestamp: s.StartTime,
|
||||
Shared: false,
|
||||
LocalEndpoint: localEndpoint,
|
||||
}
|
||||
|
||||
if s.ParentSpanID != (trace.SpanID{}) {
|
||||
id := convertSpanID(s.ParentSpanID)
|
||||
z.ParentID = &id
|
||||
}
|
||||
|
||||
if s, e := s.StartTime, s.EndTime; !s.IsZero() && !e.IsZero() {
|
||||
z.Duration = e.Sub(s)
|
||||
}
|
||||
|
||||
// construct Tags from s.Attributes and s.Status.
|
||||
if len(s.Attributes) != 0 {
|
||||
m := make(map[string]string, len(s.Attributes)+2)
|
||||
for key, value := range s.Attributes {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
m[key] = v
|
||||
case bool:
|
||||
if v {
|
||||
m[key] = "true"
|
||||
} else {
|
||||
m[key] = "false"
|
||||
}
|
||||
case int64:
|
||||
m[key] = strconv.FormatInt(v, 10)
|
||||
}
|
||||
}
|
||||
z.Tags = m
|
||||
}
|
||||
if s.Status.Code != 0 || s.Status.Message != "" {
|
||||
if z.Tags == nil {
|
||||
z.Tags = make(map[string]string, 2)
|
||||
}
|
||||
if s.Status.Code != 0 {
|
||||
z.Tags[statusCodeTagKey] = canonicalCodeString(s.Status.Code)
|
||||
}
|
||||
if s.Status.Message != "" {
|
||||
z.Tags[statusDescriptionTagKey] = s.Status.Message
|
||||
}
|
||||
}
|
||||
|
||||
// construct Annotations from s.Annotations and s.MessageEvents.
|
||||
if len(s.Annotations) != 0 || len(s.MessageEvents) != 0 {
|
||||
z.Annotations = make([]model.Annotation, 0, len(s.Annotations)+len(s.MessageEvents))
|
||||
for _, a := range s.Annotations {
|
||||
z.Annotations = append(z.Annotations, model.Annotation{
|
||||
Timestamp: a.Time,
|
||||
Value: a.Message,
|
||||
})
|
||||
}
|
||||
for _, m := range s.MessageEvents {
|
||||
a := model.Annotation{
|
||||
Timestamp: m.Time,
|
||||
}
|
||||
switch m.EventType {
|
||||
case trace.MessageEventTypeSent:
|
||||
a.Value = "SENT"
|
||||
case trace.MessageEventTypeRecv:
|
||||
a.Value = "RECV"
|
||||
default:
|
||||
a.Value = "<?>"
|
||||
}
|
||||
z.Annotations = append(z.Annotations, a)
|
||||
}
|
||||
}
|
||||
|
||||
return z
|
||||
}
|
||||
118
example-project/cmd/sidecar/tracer/main.go
Normal file
118
example-project/cmd/sidecar/tracer/main.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/sidecar/tracer/handlers"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// =========================================================================
|
||||
// Logging
|
||||
|
||||
log := log.New(os.Stdout, "TRACER : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
|
||||
defer log.Println("main : Completed")
|
||||
|
||||
// =========================================================================
|
||||
// Configuration
|
||||
|
||||
var cfg struct {
|
||||
Web struct {
|
||||
APIHost string `default:"0.0.0.0:3002" envconfig:"API_HOST"`
|
||||
DebugHost string `default:"0.0.0.0:4002" envconfig:"DEBUG_HOST"`
|
||||
ReadTimeout time.Duration `default:"5s" envconfig:"READ_TIMEOUT"`
|
||||
WriteTimeout time.Duration `default:"5s" envconfig:"WRITE_TIMEOUT"`
|
||||
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
|
||||
}
|
||||
Zipkin struct {
|
||||
Host string `default:"http://zipkin:9411/api/v2/spans" envconfig:"HOST"`
|
||||
}
|
||||
}
|
||||
|
||||
if err := envconfig.Process("TRACER", &cfg); err != nil {
|
||||
log.Fatalf("main : Parsing Config : %v", err)
|
||||
}
|
||||
|
||||
cfgJSON, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("main : Marshalling Config to JSON : %v", err)
|
||||
}
|
||||
log.Printf("config : %v\n", string(cfgJSON))
|
||||
|
||||
// =========================================================================
|
||||
// Start Debug Service. Not concerned with shutting this down when the
|
||||
// application is being shutdown.
|
||||
//
|
||||
// /debug/pprof - Added to the default mux by the net/http/pprof package.
|
||||
go func() {
|
||||
log.Printf("main : Debug Listening %s", cfg.Web.DebugHost)
|
||||
log.Printf("main : Debug Listener closed : %v", http.ListenAndServe(cfg.Web.DebugHost, http.DefaultServeMux))
|
||||
}()
|
||||
|
||||
// =========================================================================
|
||||
// Start API Service
|
||||
|
||||
// Make a channel to listen for an interrupt or terminate signal from the OS.
|
||||
// Use a buffered channel because the signal package requires it.
|
||||
shutdown := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
api := http.Server{
|
||||
Addr: cfg.Web.APIHost,
|
||||
Handler: handlers.API(shutdown, log, cfg.Zipkin.Host, cfg.Web.APIHost),
|
||||
ReadTimeout: cfg.Web.ReadTimeout,
|
||||
WriteTimeout: cfg.Web.WriteTimeout,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
|
||||
// Make a channel to listen for errors coming from the listener. Use a
|
||||
// buffered channel so the goroutine can exit if we don't collect this error.
|
||||
serverErrors := make(chan error, 1)
|
||||
|
||||
// Start the service listening for requests.
|
||||
go func() {
|
||||
log.Printf("main : API Listening %s", cfg.Web.APIHost)
|
||||
serverErrors <- api.ListenAndServe()
|
||||
}()
|
||||
|
||||
// =========================================================================
|
||||
// Shutdown
|
||||
|
||||
// Blocking main and waiting for shutdown.
|
||||
select {
|
||||
case err := <-serverErrors:
|
||||
log.Fatalf("main : Error starting server: %v", err)
|
||||
|
||||
case sig := <-shutdown:
|
||||
log.Printf("main : %v : Start shutdown..", sig)
|
||||
|
||||
// Create context for Shutdown call.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.Web.ShutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Asking listener to shutdown and load shed.
|
||||
err := api.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Printf("main : Graceful shutdown did not complete in %v : %v", cfg.Web.ShutdownTimeout, err)
|
||||
err = api.Close()
|
||||
}
|
||||
|
||||
// Log the status of this shutdown.
|
||||
switch {
|
||||
case sig == syscall.SIGSTOP:
|
||||
log.Fatal("main : Integrity issue caused shutdown")
|
||||
case err != nil:
|
||||
log.Fatalf("main : Could not stop server gracefully : %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user