1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-15 00:15:15 +02:00

Basic example cleanup

Rename sales-api to web-api and remove sales-admin
This commit is contained in:
Lee Brown
2019-05-16 18:05:39 -04:00
parent e6453bae45
commit b40d389579
25 changed files with 331 additions and 578 deletions

1
example-project/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
private.pem

View File

@ -105,7 +105,7 @@ Running `make down` will properly stop and terminate the Docker Compose session.
## About The Project
The service provides record keeping for someone running a multi-family garage sale. Authenticated users can maintain a list of products for sale.
The service provides record keeping for someone running a multi-family garage sale. Authenticated users can maintain a list of projects for sale.
<!--The service uses the following models:-->

View File

@ -1,137 +0,0 @@
// 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
}

View File

@ -42,7 +42,7 @@ func main() {
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
}
Collect struct {
From string `default:"http://sales-api:4000/debug/vars" envconfig:"FROM"`
From string `default:"http://web-api:4000/debug/vars" envconfig:"FROM"`
}
Publish struct {
To string `default:"console" envconfig:"TO"`

View File

@ -71,7 +71,7 @@ func (z *Zipkin) Publish(ctx context.Context, w http.ResponseWriter, r *http.Req
// 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)
le, err := newEndpoint("web-api", z.localHost)
if err != nil {
return err
}

View File

@ -6,48 +6,48 @@ import (
"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"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
"github.com/pkg/errors"
"go.opencensus.io/trace"
)
// Product represents the Product API method handler set.
type Product struct {
// Project represents the Project API method handler set.
type Project 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")
// List returns all the existing projects in the system.
func (p *Project) List(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Project.List")
defer span.End()
dbConn := p.MasterDB.Copy()
defer dbConn.Close()
products, err := product.List(ctx, dbConn)
projects, err := project.List(ctx, dbConn)
if err != nil {
return err
}
return web.Respond(ctx, w, products, http.StatusOK)
return web.Respond(ctx, w, projects, 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")
// Retrieve returns the specified project from the system.
func (p *Project) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Project.Retrieve")
defer span.End()
dbConn := p.MasterDB.Copy()
defer dbConn.Close()
prod, err := product.Retrieve(ctx, dbConn, params["id"])
prod, err := project.Retrieve(ctx, dbConn, params["id"])
if err != nil {
switch err {
case product.ErrInvalidID:
case project.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case product.ErrNotFound:
case project.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
default:
return errors.Wrapf(err, "ID: %s", params["id"])
@ -57,9 +57,9 @@ func (p *Product) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.R
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")
// Create inserts a new project into the system.
func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Project.Create")
defer span.End()
dbConn := p.MasterDB.Copy()
@ -70,22 +70,22 @@ func (p *Product) Create(ctx context.Context, w http.ResponseWriter, r *http.Req
return web.NewShutdownError("web value missing from context")
}
var np product.NewProduct
var np project.NewProject
if err := web.Decode(r, &np); err != nil {
return errors.Wrap(err, "")
}
nUsr, err := product.Create(ctx, dbConn, &np, v.Now)
nUsr, err := project.Create(ctx, dbConn, &np, v.Now)
if err != nil {
return errors.Wrapf(err, "Product: %+v", &np)
return errors.Wrapf(err, "Project: %+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")
// Update updates the specified project in the system.
func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Project.Update")
defer span.End()
dbConn := p.MasterDB.Copy()
@ -96,17 +96,17 @@ func (p *Product) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
return web.NewShutdownError("web value missing from context")
}
var up product.UpdateProduct
var up project.UpdateProject
if err := web.Decode(r, &up); err != nil {
return errors.Wrap(err, "")
}
err := product.Update(ctx, dbConn, params["id"], up, v.Now)
err := project.Update(ctx, dbConn, params["id"], up, v.Now)
if err != nil {
switch err {
case product.ErrInvalidID:
case project.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case product.ErrNotFound:
case project.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
default:
return errors.Wrapf(err, "ID: %s Update: %+v", params["id"], up)
@ -116,20 +116,20 @@ func (p *Product) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
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")
// Delete removes the specified project from the system.
func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Project.Delete")
defer span.End()
dbConn := p.MasterDB.Copy()
defer dbConn.Close()
err := product.Delete(ctx, dbConn, params["id"])
err := project.Delete(ctx, dbConn, params["id"])
if err != nil {
switch err {
case product.ErrInvalidID:
case project.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case product.ErrNotFound:
case project.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
default:
return errors.Wrapf(err, "Id: %s", params["id"])

View File

@ -37,15 +37,15 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *db.DB, authenticato
// This route is not authenticated
app.Handle("GET", "/v1/users/token", u.Token)
// Register product and sale endpoints.
p := Product{
// Register project and sale endpoints.
p := Project{
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))
app.Handle("GET", "/v1/projects", p.List, mid.Authenticate(authenticator))
app.Handle("POST", "/v1/projects", p.Create, mid.Authenticate(authenticator))
app.Handle("GET", "/v1/projects/:id", p.Retrieve, mid.Authenticate(authenticator))
app.Handle("PUT", "/v1/projects/:id", p.Update, mid.Authenticate(authenticator))
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator))
return app
}

View File

@ -14,7 +14,7 @@ import (
"syscall"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/sales-api/handlers"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-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"
@ -45,7 +45,7 @@ func main() {
// =========================================================================
// Logging
log := log.New(os.Stdout, "SALES : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
log := log.New(os.Stdout, "WEB_APP : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
// =========================================================================
// Configuration
@ -75,7 +75,7 @@ func main() {
}
}
if err := envconfig.Process("SALES", &cfg); err != nil {
if err := envconfig.Process("WEB_APP", &cfg); err != nil {
log.Fatalf("main : Parsing Config : %v", err)
}

View File

@ -10,38 +10,38 @@ import (
"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"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
"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) {
// TestProjects is the entry point for the projects
func TestProjects(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)
t.Run("getProjects200Empty", getProjects200Empty)
t.Run("postProject400", postProject400)
t.Run("postProject401", postProject401)
t.Run("getProject404", getProject404)
t.Run("getProject400", getProject400)
t.Run("deleteProject404", deleteProject404)
t.Run("putProject404", putProject404)
t.Run("crudProjects", crudProject)
}
// getProducts200Empty validates an empty products list can be retrieved with the endpoint.
func getProducts200Empty(t *testing.T) {
r := httptest.NewRequest("GET", "/v1/products", nil)
// getProjects200Empty validates an empty projects list can be retrieved with the endpoint.
func getProjects200Empty(t *testing.T) {
r := httptest.NewRequest("GET", "/v1/projects", 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("Given the need to fetch an empty list of projects with the projects endpoint.")
{
t.Log("\tTest 0:\tWhen fetching an empty product list.")
t.Log("\tTest 0:\tWhen fetching an empty project 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)
@ -60,19 +60,19 @@ func getProducts200Empty(t *testing.T) {
}
}
// 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(`{}`))
// postProject400 validates a project can't be created with the endpoint
// unless a valid project document is submitted.
func postProject400(t *testing.T) {
r := httptest.NewRequest("POST", "/v1/projects", 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("Given the need to validate a new project can't be created with an invalid document.")
{
t.Log("\tTest 0:\tWhen using an incomplete product value.")
t.Log("\tTest 0:\tWhen using an incomplete project 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)
@ -110,10 +110,10 @@ func postProduct400(t *testing.T) {
}
}
// postProduct401 validates a product can't be created with the endpoint
// postProject401 validates a project can't be created with the endpoint
// unless the user is authenticated
func postProduct401(t *testing.T) {
np := product.NewProduct{
func postProject401(t *testing.T) {
np := project.NewProject{
Name: "Comic Books",
Cost: 25,
Quantity: 60,
@ -124,16 +124,16 @@ func postProduct401(t *testing.T) {
t.Fatal(err)
}
r := httptest.NewRequest("POST", "/v1/products", bytes.NewBuffer(body))
r := httptest.NewRequest("POST", "/v1/projects", 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("Given the need to validate a new project can't be created with an invalid document.")
{
t.Log("\tTest 0:\tWhen using an incomplete product value.")
t.Log("\tTest 0:\tWhen using an incomplete project 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)
@ -143,20 +143,20 @@ func postProduct401(t *testing.T) {
}
}
// getProduct400 validates a product request for a malformed id.
func getProduct400(t *testing.T) {
// getProject400 validates a project request for a malformed id.
func getProject400(t *testing.T) {
id := "12345"
r := httptest.NewRequest("GET", "/v1/products/"+id, nil)
r := httptest.NewRequest("GET", "/v1/projects/"+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.Log("Given the need to validate getting a project with a malformed id.")
{
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
t.Logf("\tTest 0:\tWhen using the new project %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)
@ -175,20 +175,20 @@ func getProduct400(t *testing.T) {
}
}
// getProduct404 validates a product request for a product that does not exist with the endpoint.
func getProduct404(t *testing.T) {
// getProject404 validates a project request for a project that does not exist with the endpoint.
func getProject404(t *testing.T) {
id := bson.NewObjectId().Hex()
r := httptest.NewRequest("GET", "/v1/products/"+id, nil)
r := httptest.NewRequest("GET", "/v1/projects/"+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.Log("Given the need to validate getting a project with an unknown id.")
{
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
t.Logf("\tTest 0:\tWhen using the new project %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)
@ -207,20 +207,20 @@ func getProduct404(t *testing.T) {
}
}
// deleteProduct404 validates deleting a product that does not exist.
func deleteProduct404(t *testing.T) {
// deleteProject404 validates deleting a project that does not exist.
func deleteProject404(t *testing.T) {
id := bson.NewObjectId().Hex()
r := httptest.NewRequest("DELETE", "/v1/products/"+id, nil)
r := httptest.NewRequest("DELETE", "/v1/projects/"+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.Log("Given the need to validate deleting a project that does not exist.")
{
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
t.Logf("\tTest 0:\tWhen using the new project %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)
@ -239,9 +239,9 @@ func deleteProduct404(t *testing.T) {
}
}
// putProduct404 validates updating a product that does not exist.
func putProduct404(t *testing.T) {
up := product.UpdateProduct{
// putProject404 validates updating a project that does not exist.
func putProject404(t *testing.T) {
up := project.UpdateProject{
Name: tests.StringPointer("Nonexistent"),
}
@ -252,16 +252,16 @@ func putProduct404(t *testing.T) {
t.Fatal(err)
}
r := httptest.NewRequest("PUT", "/v1/products/"+id, bytes.NewBuffer(body))
r := httptest.NewRequest("PUT", "/v1/projects/"+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.Log("Given the need to validate updating a project that does not exist.")
{
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
t.Logf("\tTest 0:\tWhen using the new project %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)
@ -280,18 +280,18 @@ func putProduct404(t *testing.T) {
}
}
// crudProduct performs a complete test of CRUD against the api.
func crudProduct(t *testing.T) {
p := postProduct201(t)
defer deleteProduct204(t, p.ID.Hex())
// crudProject performs a complete test of CRUD against the api.
func crudProject(t *testing.T) {
p := postProject201(t)
defer deleteProject204(t, p.ID.Hex())
getProduct200(t, p.ID.Hex())
putProduct204(t, p.ID.Hex())
getProject200(t, p.ID.Hex())
putProject204(t, p.ID.Hex())
}
// postProduct201 validates a product can be created with the endpoint.
func postProduct201(t *testing.T) product.Product {
np := product.NewProduct{
// postProject201 validates a project can be created with the endpoint.
func postProject201(t *testing.T) project.Project {
np := project.NewProject{
Name: "Comic Books",
Cost: 25,
Quantity: 60,
@ -302,7 +302,7 @@ func postProduct201(t *testing.T) product.Product {
t.Fatal(err)
}
r := httptest.NewRequest("POST", "/v1/products", bytes.NewBuffer(body))
r := httptest.NewRequest("POST", "/v1/projects", bytes.NewBuffer(body))
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
@ -310,11 +310,11 @@ func postProduct201(t *testing.T) product.Product {
a.ServeHTTP(w, r)
// p is the value we will return.
var p product.Product
var p project.Project
t.Log("Given the need to create a new product with the products endpoint.")
t.Log("Given the need to create a new project with the projects endpoint.")
{
t.Log("\tTest 0:\tWhen using the declared product value.")
t.Log("\tTest 0:\tWhen using the declared project 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)
@ -342,18 +342,18 @@ func postProduct201(t *testing.T) product.Product {
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)
// deleteProject200 validates deleting a project that does exist.
func deleteProject204(t *testing.T, id string) {
r := httptest.NewRequest("DELETE", "/v1/projects/"+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.Log("Given the need to validate deleting a project that does exist.")
{
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
t.Logf("\tTest 0:\tWhen using the new project %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)
@ -363,25 +363,25 @@ func deleteProduct204(t *testing.T, id string) {
}
}
// getProduct200 validates a product request for an existing id.
func getProduct200(t *testing.T, id string) {
r := httptest.NewRequest("GET", "/v1/products/"+id, nil)
// getProject200 validates a project request for an existing id.
func getProject200(t *testing.T, id string) {
r := httptest.NewRequest("GET", "/v1/projects/"+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.Log("Given the need to validate getting a project that exists.")
{
t.Logf("\tTest 0:\tWhen using the new product %s.", id)
t.Logf("\tTest 0:\tWhen using the new project %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
var p project.Project
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)
}
@ -402,26 +402,26 @@ func getProduct200(t *testing.T, id string) {
}
}
// putProduct204 validates updating a product that does exist.
func putProduct204(t *testing.T, id string) {
// putProject204 validates updating a project that does exist.
func putProject204(t *testing.T, id string) {
body := `{"name": "Graphic Novels", "cost": 100}`
r := httptest.NewRequest("PUT", "/v1/products/"+id, strings.NewReader(body))
r := httptest.NewRequest("PUT", "/v1/projects/"+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("Given the need to update a project with the projects endpoint.")
{
t.Log("\tTest 0:\tWhen using the modified product value.")
t.Log("\tTest 0:\tWhen using the modified project 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)
r = httptest.NewRequest("GET", "/v1/projects/"+id, nil)
w = httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
@ -433,7 +433,7 @@ func putProduct204(t *testing.T, id string) {
}
t.Logf("\t%s\tShould receive a status code of 200 for the retrieve.", tests.Success)
var ru product.Product
var ru project.Project
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)
}

View File

@ -8,7 +8,7 @@ import (
"testing"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/sales-api/handlers"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-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"

View File

@ -21,17 +21,17 @@ services:
command: --bind_ip 0.0.0.0
# This is the core CRUD based service.
sales-api:
container_name: sales-api
web-api:
container_name: web-api
networks:
- shared-network
image: gcr.io/sales-api/sales-api-amd64:1.0
image: gcr.io/web-api/web-api-amd64:1.0
ports:
- 3000:3000 # CRUD API
- 4000:4000 # DEBUG API
environment:
- SALES_AUTH_KEY_ID=1
# - SALES_DB_HOST=got:got2015@ds039441.mongolab.com:39441/gotraining
- WEB_APP_AUTH_KEY_ID=1
# - WEB_APP_DB_HOST=got:got2015@ds039441.mongolab.com:39441/gotraining
# - GODEBUG=gctrace=1
# This sidecar publishes metrics to the console by default.
@ -39,7 +39,7 @@ services:
container_name: metrics
networks:
- shared-network
image: gcr.io/sales-api/metrics-amd64:1.0
image: gcr.io/web-api/metrics-amd64:1.0
ports:
- 3001:3001 # EXPVAR API
- 4001:4001 # DEBUG API
@ -49,12 +49,12 @@ services:
container_name: tracer
networks:
- shared-network
image: gcr.io/sales-api/tracer-amd64:1.0
image: gcr.io/web-api/tracer-amd64:1.0
ports:
- 3002:3002 # TRACER API
- 4002:4002 # DEBUG API
# environment:
# - SALES_ZIPKIN_HOST=http://zipkin:9411/api/v2/spans
# - WEB_APP_ZIPKIN_HOST=http://zipkin:9411/api/v2/spans
# This sidecar allows for the viewing of traces.
zipkin:

View File

@ -1,24 +0,0 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mongo
spec:
replicas: 1
strategy: {}
template:
metadata:
name: mongo
labels:
database: mongo
spec:
containers:
- name: mongo
image: mongo:3-jessie
args:
- --bind_ip
- 0.0.0.0
ports:
- name: mongo
containerPort: 27017
resources: {}
status: {}

View File

@ -1,56 +0,0 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: sales-api
spec:
replicas: 1
strategy: {}
template:
metadata:
name: sales-api
labels:
service: sales-api
spec:
containers:
- name: zipkin
image: openzipkin/zipkin:2.11
ports:
- name: zipkin
containerPort: 9411
resources: {}
- name: sales-api
image: gcr.io/sales-api/sales-api-amd64:1.0
env:
- name: SALES_TRACE_HOST
value: "http://localhost:3002/v1/publish"
- name: SALES_AUTH_KEY_ID
value: "1"
ports:
- name: sales-api
containerPort: 3000
- name: debug
containerPort: 4000
resources: {}
- name: metrics
image: gcr.io/sales-api/metrics-amd64:1.0
env:
- name: METRICS_COLLECT_FROM
value: "http://localhost:4000/debug/vars"
ports:
- name: metrics
containerPort: 3001
- name: debug
containerPort: 4001
resources: {}
- name: tracer
image: gcr.io/sales-api/tracer-amd64:1.0
env:
- name: TRACER_ZIPKIN_HOST
value: "http://localhost:9411/api/v2/spans"
ports:
- name: tracer
containerPort: 3002
- name: debug
containerPort: 4002
resources: {}
status: {}

View File

@ -1,11 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: mongo
spec:
selector:
database: mongo
ports:
- name: "db"
port: 27017
targetPort: 27017

View File

@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: sales-api
spec:
selector:
service: sales-api
ports:
- name: "zipkin"
port: 9411
targetPort: 9411
- name: "sales-api"
port: 3000
targetPort: 3000
- name: "metrics"
port: 3001
targetPort: 3001
status:
loadBalancer: {}

View File

@ -20,7 +20,7 @@ import (
type KeyFunc func(keyID string) (*rsa.PublicKey, error)
// NewSingleKeyFunc is a simple implementation of KeyFunc that only ever
// supports one key. This is easy for development but in production should be
// supports one key. This is easy for development but in projection should be
// replaced with a caching layer that calls a JWKS endpoint.
func NewSingleKeyFunc(id string, key *rsa.PublicKey) KeyFunc {
return func(kid string) (*rsa.PublicKey, error) {

View File

@ -84,7 +84,7 @@ func TestApply(t *testing.T) {
Host string `default:"mongo:27017/gotraining" flag:"h"`
Insecure bool `flag:"i"`
}
osArgs := []string{"./sales-api", "-i", "-a", "0.0.1.1:5000", "--web_batchsize", "300", "--dialtimeout", "10s"}
osArgs := []string{"./web-api", "-i", "-a", "0.0.1.1:5000", "--web_batchsize", "300", "--dialtimeout", "10s"}
expected := `{"Web":{"APIHost":"0.0.1.1:5000","BatchSize":300,"ReadTimeout":0},"DialTimeout":10000000000,"Host":"","Insecure":true}`
t.Log("Given the need to validate we can apply overrides a struct value.")

View File

@ -1,129 +0,0 @@
package product_test
import (
"os"
"testing"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/product"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
)
var test *tests.Test
// 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()
return m.Run()
}
// TestProduct validates the full set of CRUD operations on Product values.
func TestProduct(t *testing.T) {
defer tests.Recover(t)
t.Log("Given the need to work with Product records.")
{
t.Log("\tWhen handling a single Product.")
{
ctx := tests.Context()
dbConn := test.MasterDB.Copy()
defer dbConn.Close()
np := product.NewProduct{
Name: "Comic Books",
Cost: 25,
Quantity: 60,
}
p, err := product.Create(ctx, dbConn, &np, time.Now().UTC())
if err != nil {
t.Fatalf("\t%s\tShould be able to create a product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to create a product.", tests.Success)
savedP, err := product.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve product by ID: %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve product by ID.", tests.Success)
if diff := cmp.Diff(p, savedP); diff != "" {
t.Fatalf("\t%s\tShould get back the same product. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get back the same product.", tests.Success)
upd := product.UpdateProduct{
Name: tests.StringPointer("Comics"),
Cost: tests.IntPointer(50),
Quantity: tests.IntPointer(40),
}
if err := product.Update(ctx, dbConn, p.ID.Hex(), upd, time.Now().UTC()); err != nil {
t.Fatalf("\t%s\tShould be able to update product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to update product.", tests.Success)
savedP, err = product.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve updated product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve updated product.", tests.Success)
// Build a product matching what we expect to see. We just use the
// modified time from the database.
want := &product.Product{
ID: p.ID,
Name: *upd.Name,
Cost: *upd.Cost,
Quantity: *upd.Quantity,
DateCreated: p.DateCreated,
DateModified: savedP.DateModified,
}
if diff := cmp.Diff(want, savedP); diff != "" {
t.Fatalf("\t%s\tShould get back the same product. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get back the same product.", tests.Success)
upd = product.UpdateProduct{
Name: tests.StringPointer("Graphic Novels"),
}
if err := product.Update(ctx, dbConn, p.ID.Hex(), upd, time.Now().UTC()); err != nil {
t.Fatalf("\t%s\tShould be able to update just some fields of product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to update just some fields of product.", tests.Success)
savedP, err = product.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve updated product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve updated product.", tests.Success)
if savedP.Name != *upd.Name {
t.Fatalf("\t%s\tShould be able to see updated Name field : got %q want %q.", tests.Failed, savedP.Name, *upd.Name)
} else {
t.Logf("\t%s\tShould be able to see updated Name field.", tests.Success)
}
if err := product.Delete(ctx, dbConn, p.ID.Hex()); err != nil {
t.Fatalf("\t%s\tShould be able to delete product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to delete product.", tests.Success)
savedP, err = product.Retrieve(ctx, dbConn, p.ID.Hex())
if errors.Cause(err) != product.ErrNotFound {
t.Fatalf("\t%s\tShould NOT be able to retrieve deleted product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould NOT be able to retrieve deleted product.", tests.Success)
}
}
}

View File

@ -1,4 +1,4 @@
package product
package project
import (
"time"
@ -6,37 +6,37 @@ import (
"gopkg.in/mgo.v2/bson"
)
// Product is an item we sell.
type Product struct {
// Project is an item we sell.
type Project struct {
ID bson.ObjectId `bson:"_id" json:"id"` // Unique identifier.
Name string `bson:"name" json:"name"` // Display name of the product.
Name string `bson:"name" json:"name"` // Display name of the project.
Cost int `bson:"cost" json:"cost"` // Price for one item in cents.
Quantity int `bson:"quantity" json:"quantity"` // Original number of items available.
DateCreated time.Time `bson:"date_created" json:"date_created"` // When the product was added.
DateModified time.Time `bson:"date_modified" json:"date_modified"` // When the product record was lost modified.
DateCreated time.Time `bson:"date_created" json:"date_created"` // When the project was added.
DateModified time.Time `bson:"date_modified" json:"date_modified"` // When the project record was lost modified.
}
// NewProduct is what we require from clients when adding a Product.
type NewProduct struct {
// NewProject is what we require from clients when adding a Project.
type NewProject struct {
Name string `json:"name" validate:"required"`
Cost int `json:"cost" validate:"required,gte=0"`
Quantity int `json:"quantity" validate:"required,gte=1"`
}
// UpdateProduct defines what information may be provided to modify an
// existing Product. All fields are optional so clients can send just the
// UpdateProject defines what information may be provided to modify an
// existing Project. All fields are optional so clients can send just the
// fields they want changed. It uses pointer fields so we can differentiate
// between a field that was not provided and a field that was provided as
// explicitly blank. Normally we do not want to use pointers to basic types but
// we make exceptions around marshalling/unmarshalling.
type UpdateProduct struct {
type UpdateProject struct {
Name *string `json:"name"`
Cost *int `json:"cost" validate:"omitempty,gte=0"`
Quantity *int `json:"quantity" validate:"omitempty,gte=1"`
}
// Sale represents a transaction where we sold some quantity of a
// Product.
// Project.
type Sale struct{}
// NewSale defines what we require when creating a Sale record.

View File

@ -1,4 +1,4 @@
package product
package project
import (
"context"
@ -12,7 +12,7 @@ import (
"gopkg.in/mgo.v2/bson"
)
const productsCollection = "products"
const projectsCollection = "projects"
var (
// ErrNotFound abstracts the mgo not found error.
@ -22,26 +22,26 @@ var (
ErrInvalidID = errors.New("ID is not in its proper form")
)
// List retrieves a list of existing products from the database.
func List(ctx context.Context, dbConn *db.DB) ([]Product, error) {
ctx, span := trace.StartSpan(ctx, "internal.product.List")
// List retrieves a list of existing projects from the database.
func List(ctx context.Context, dbConn *db.DB) ([]Project, error) {
ctx, span := trace.StartSpan(ctx, "internal.project.List")
defer span.End()
p := []Product{}
p := []Project{}
f := func(collection *mgo.Collection) error {
return collection.Find(nil).All(&p)
}
if err := dbConn.Execute(ctx, productsCollection, f); err != nil {
return nil, errors.Wrap(err, "db.products.find()")
if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
return nil, errors.Wrap(err, "db.projects.find()")
}
return p, nil
}
// Retrieve gets the specified product from the database.
func Retrieve(ctx context.Context, dbConn *db.DB, id string) (*Product, error) {
ctx, span := trace.StartSpan(ctx, "internal.product.Retrieve")
// Retrieve gets the specified project from the database.
func Retrieve(ctx context.Context, dbConn *db.DB, id string) (*Project, error) {
ctx, span := trace.StartSpan(ctx, "internal.project.Retrieve")
defer span.End()
if !bson.IsObjectIdHex(id) {
@ -50,30 +50,30 @@ func Retrieve(ctx context.Context, dbConn *db.DB, id string) (*Product, error) {
q := bson.M{"_id": bson.ObjectIdHex(id)}
var p *Product
var p *Project
f := func(collection *mgo.Collection) error {
return collection.Find(q).One(&p)
}
if err := dbConn.Execute(ctx, productsCollection, f); err != nil {
if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
if err == mgo.ErrNotFound {
return nil, ErrNotFound
}
return nil, errors.Wrap(err, fmt.Sprintf("db.products.find(%s)", db.Query(q)))
return nil, errors.Wrap(err, fmt.Sprintf("db.projects.find(%s)", db.Query(q)))
}
return p, nil
}
// Create inserts a new product into the database.
func Create(ctx context.Context, dbConn *db.DB, cp *NewProduct, now time.Time) (*Product, error) {
ctx, span := trace.StartSpan(ctx, "internal.product.Create")
// Create inserts a new project into the database.
func Create(ctx context.Context, dbConn *db.DB, cp *NewProject, now time.Time) (*Project, error) {
ctx, span := trace.StartSpan(ctx, "internal.project.Create")
defer span.End()
// Mongo truncates times to milliseconds when storing. We and do the same
// here so the value we return is consistent with what we store.
now = now.Truncate(time.Millisecond)
p := Product{
p := Project{
ID: bson.NewObjectId(),
Name: cp.Name,
Cost: cp.Cost,
@ -85,16 +85,16 @@ func Create(ctx context.Context, dbConn *db.DB, cp *NewProduct, now time.Time) (
f := func(collection *mgo.Collection) error {
return collection.Insert(&p)
}
if err := dbConn.Execute(ctx, productsCollection, f); err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("db.products.insert(%s)", db.Query(&p)))
if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("db.projects.insert(%s)", db.Query(&p)))
}
return &p, nil
}
// Update replaces a product document in the database.
func Update(ctx context.Context, dbConn *db.DB, id string, upd UpdateProduct, now time.Time) error {
ctx, span := trace.StartSpan(ctx, "internal.product.Update")
// Update replaces a project document in the database.
func Update(ctx context.Context, dbConn *db.DB, id string, upd UpdateProject, now time.Time) error {
ctx, span := trace.StartSpan(ctx, "internal.project.Update")
defer span.End()
if !bson.IsObjectIdHex(id) {
@ -126,7 +126,7 @@ func Update(ctx context.Context, dbConn *db.DB, id string, upd UpdateProduct, no
f := func(collection *mgo.Collection) error {
return collection.Update(q, m)
}
if err := dbConn.Execute(ctx, productsCollection, f); err != nil {
if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
if err == mgo.ErrNotFound {
return ErrNotFound
}
@ -136,9 +136,9 @@ func Update(ctx context.Context, dbConn *db.DB, id string, upd UpdateProduct, no
return nil
}
// Delete removes a product from the database.
// Delete removes a project from the database.
func Delete(ctx context.Context, dbConn *db.DB, id string) error {
ctx, span := trace.StartSpan(ctx, "internal.product.Delete")
ctx, span := trace.StartSpan(ctx, "internal.project.Delete")
defer span.End()
if !bson.IsObjectIdHex(id) {
@ -150,11 +150,11 @@ func Delete(ctx context.Context, dbConn *db.DB, id string) error {
f := func(collection *mgo.Collection) error {
return collection.Remove(q)
}
if err := dbConn.Execute(ctx, productsCollection, f); err != nil {
if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
if err == mgo.ErrNotFound {
return ErrNotFound
}
return errors.Wrap(err, fmt.Sprintf("db.products.remove(%v)", q))
return errors.Wrap(err, fmt.Sprintf("db.projects.remove(%v)", q))
}
return nil

View File

@ -0,0 +1,129 @@
package project_test
import (
"os"
"testing"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
)
var test *tests.Test
// 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()
return m.Run()
}
// TestProject validates the full set of CRUD operations on Project values.
func TestProject(t *testing.T) {
defer tests.Recover(t)
t.Log("Given the need to work with Project records.")
{
t.Log("\tWhen handling a single Project.")
{
ctx := tests.Context()
dbConn := test.MasterDB.Copy()
defer dbConn.Close()
np := project.NewProject{
Name: "Comic Books",
Cost: 25,
Quantity: 60,
}
p, err := project.Create(ctx, dbConn, &np, time.Now().UTC())
if err != nil {
t.Fatalf("\t%s\tShould be able to create a project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to create a project.", tests.Success)
savedP, err := project.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve project by ID: %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve project by ID.", tests.Success)
if diff := cmp.Diff(p, savedP); diff != "" {
t.Fatalf("\t%s\tShould get back the same project. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get back the same project.", tests.Success)
upd := project.UpdateProject{
Name: tests.StringPointer("Comics"),
Cost: tests.IntPointer(50),
Quantity: tests.IntPointer(40),
}
if err := project.Update(ctx, dbConn, p.ID.Hex(), upd, time.Now().UTC()); err != nil {
t.Fatalf("\t%s\tShould be able to update project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to update project.", tests.Success)
savedP, err = project.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve updated project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve updated project.", tests.Success)
// Build a project matching what we expect to see. We just use the
// modified time from the database.
want := &project.Project{
ID: p.ID,
Name: *upd.Name,
Cost: *upd.Cost,
Quantity: *upd.Quantity,
DateCreated: p.DateCreated,
DateModified: savedP.DateModified,
}
if diff := cmp.Diff(want, savedP); diff != "" {
t.Fatalf("\t%s\tShould get back the same project. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get back the same project.", tests.Success)
upd = project.UpdateProject{
Name: tests.StringPointer("Graphic Novels"),
}
if err := project.Update(ctx, dbConn, p.ID.Hex(), upd, time.Now().UTC()); err != nil {
t.Fatalf("\t%s\tShould be able to update just some fields of project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to update just some fields of project.", tests.Success)
savedP, err = project.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve updated project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve updated project.", tests.Success)
if savedP.Name != *upd.Name {
t.Fatalf("\t%s\tShould be able to see updated Name field : got %q want %q.", tests.Failed, savedP.Name, *upd.Name)
} else {
t.Logf("\t%s\tShould be able to see updated Name field.", tests.Success)
}
if err := project.Delete(ctx, dbConn, p.ID.Hex()); err != nil {
t.Fatalf("\t%s\tShould be able to delete project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to delete project.", tests.Success)
savedP, err = project.Retrieve(ctx, dbConn, p.ID.Hex())
if errors.Cause(err) != project.ErrNotFound {
t.Fatalf("\t%s\tShould NOT be able to retrieve deleted project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould NOT be able to retrieve deleted project.", tests.Success)
}
}
}

View File

@ -1,6 +1,6 @@
SHELL := /bin/bash
all: keys sales-api metrics tracer
all: keys web-api metrics tracer
keys:
go run ./cmd/sales-admin/main.go --cmd keygen
@ -8,10 +8,10 @@ keys:
admin:
go run ./cmd/sales-admin/main.go --cmd useradd --user_email admin@example.com --user_password gophers
sales-api:
web-api:
docker build \
-t gcr.io/sales-api/sales-api-amd64:1.0 \
--build-arg PACKAGE_NAME=sales-api \
-t gcr.io/web-api/web-api-amd64:1.0 \
--build-arg PACKAGE_NAME=web-api \
--build-arg VCS_REF=`git rev-parse HEAD` \
--build-arg BUILD_DATE=`date -u +”%Y-%m-%dT%H:%M:%SZ”` \
.
@ -19,7 +19,7 @@ sales-api:
metrics:
docker build \
-t gcr.io/sales-api/metrics-amd64:1.0 \
-t gcr.io/web-api/metrics-amd64:1.0 \
--build-arg PACKAGE_NAME=metrics \
--build-arg PACKAGE_PREFIX=sidecar/ \
--build-arg VCS_REF=`git rev-parse HEAD` \
@ -28,9 +28,8 @@ metrics:
docker system prune -f
tracer:
cd "$$GOPATH/src/geeks-accelerator/oss/saas-starter-kit/example-project"
docker build \
-t gcr.io/sales-api/tracer-amd64:1.0 \
-t gcr.io/web-api/tracer-amd64:1.0 \
--build-arg PACKAGE_NAME=tracer \
--build-arg PACKAGE_PREFIX=sidecar/ \
--build-arg VCS_REF=`git rev-parse HEAD` \
@ -61,27 +60,27 @@ remove-all:
# GKE
config:
@echo Setting environment for sales-api
gcloud config set project sales-api
@echo Setting environment for web-api
gcloud config set project web-api
gcloud config set compute/zone us-central1-b
gcloud auth configure-docker
@echo ======================================================================
project:
gcloud projects create sales-api
gcloud beta billing projects link sales-api --billing-account=$(ACCOUNT_ID)
gcloud projects create web-api
gcloud beta billing projects link web-api --billing-account=$(ACCOUNT_ID)
gcloud services enable container.googleapis.com
@echo ======================================================================
cluster:
gcloud container clusters create sales-api-cluster --num-nodes=2 --machine-type=n1-standard-2
gcloud container clusters create web-api-cluster --num-nodes=2 --machine-type=n1-standard-2
gcloud compute instances list
@echo ======================================================================
upload:
docker push gcr.io/sales-api/sales-api-amd64:1.0
docker push gcr.io/sales-api/metrics-amd64:1.0
docker push gcr.io/sales-api/tracer-amd64:1.0
docker push gcr.io/web-api/web-api-amd64:1.0
docker push gcr.io/web-api/metrics-amd64:1.0
docker push gcr.io/web-api/tracer-amd64:1.0
@echo ======================================================================
database:
@ -90,8 +89,8 @@ database:
@echo ======================================================================
services:
kubectl create -f gke-deploy-sales-api.yaml
kubectl expose -f gke-expose-sales-api.yaml --type=LoadBalancer
kubectl create -f gke-deploy-web-api.yaml
kubectl expose -f gke-expose-web-api.yaml --type=LoadBalancer
@echo ======================================================================
shell:
@ -102,15 +101,15 @@ status:
gcloud container clusters list
kubectl get nodes
kubectl get pods
kubectl get services sales-api
kubectl get services web-api
@echo ======================================================================
delete:
kubectl delete services sales-api
kubectl delete deployment sales-api
gcloud container clusters delete sales-api-cluster
gcloud projects delete sales-api
docker image remove gcr.io/sales-api/sales-api-amd64:1.0
docker image remove gcr.io/sales-api/metrics-amd64:1.0
docker image remove gcr.io/sales-api/tracer-amd64:1.0
kubectl delete services web-api
kubectl delete deployment web-api
gcloud container clusters delete web-api-cluster
gcloud projects delete web-api
docker image remove gcr.io/web-api/web-api-amd64:1.0
docker image remove gcr.io/web-api/metrics-amd64:1.0
docker image remove gcr.io/web-api/tracer-amd64:1.0
@echo ======================================================================