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

Finish inital schema migration script

This commit is contained in:
Lee Brown
2019-05-23 14:01:24 -05:00
parent 790c7b3255
commit 271bf37c5d
16 changed files with 749 additions and 288 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
aws.lee
aws.*

View File

@ -1 +1 @@
private.pem
.env_docker_compose

View File

@ -143,6 +143,24 @@ To make authenticated requests put the token in the `Authorization` header with
$ curl -H "Authorization: Bearer ${TOKEN}" http://localhost:3000/v1/users
```
## Making db calls
Currently postgres is only supported for sqlxmigrate. MySQL should be easy to add after determing
better method for abstracting the create table and other SQL statements from the main
testing logic.
### bindvars
When making new packages that use sqlx, bind vars for mysql are `?` where as postgres is `$1`.
To database agnostic, sqlx supports using `?` for all queries and exposes the method `Rebind` to
remap the placeholders to the correct database.
```go
sqlQueryStr = db.Rebind(sqlQueryStr)
```
For additional details refer to https://jmoiron.github.io/sqlx/#bindvars
## What's Next
We are in the process of writing more documentation about this code. Classes are being finalized as part of the Ultimate series.
@ -150,6 +168,7 @@ We are in the process of writing more documentation about this code. Classes are
## AWS Permissions
Base required permissions
@ -171,3 +190,24 @@ Additional permissions required for unittests
secretsmanager:DeleteSecret
```
### TODO:
additianal info required here in readme
need to copy sample.env_docker_compose to .env_docker_compose and defined your aws configs for docker-compose
/*
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
*/

View File

@ -7,14 +7,17 @@ import (
"log"
"net/http"
_ "net/http/pprof"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/go-redis/redis"
"github.com/lib/pq"
"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"
itrace "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/trace"
"github.com/aws/aws-sdk-go/aws"
@ -23,21 +26,11 @@ import (
"github.com/kelseyhightower/envconfig"
"go.opencensus.io/trace"
awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws"
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
redistrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
)
/*
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"
@ -53,14 +46,56 @@ func main() {
var cfg struct {
Env string `default:"dev" envconfig:"ENV"`
HTTP struct {
Host string `default:"0.0.0.0:3001" envconfig:"HTTP_HOST"`
DebugHost string `default:"0.0.0.0:4000" envconfig:"HTTP_DEBUG_HOST"`
ReadTimeout time.Duration `default:"5s" envconfig:"HTTP_READ_TIMEOUT"`
WriteTimeout time.Duration `default:"5s" envconfig:"HTTP_WRITE_TIMEOUT"`
ShutdownTimeout time.Duration `default:"5s" envconfig:"HTTP_SHUTDOWN_TIMEOUT"`
Host string `default:"0.0.0.0:3000" envconfig:"HTTP_HOST"`
ReadTimeout time.Duration `default:"10s" envconfig:"HTTP_READ_TIMEOUT"`
WriteTimeout time.Duration `default:"10s" envconfig:"HTTP_WRITE_TIMEOUT"`
}
HTTPS struct {
Host string `default:"" envconfig:"HTTPS_HOST"`
ReadTimeout time.Duration `default:"5s" envconfig:"HTTPS_READ_TIMEOUT"`
WriteTimeout time.Duration `default:"5s" envconfig:"HTTPS_WRITE_TIMEOUT"`
}
App struct {
Name string `default:"web-app" envconfig:"APP_NAME"`
Name string `default:"web-api" envconfig:"APP_NAME"`
BaseUrl string `default:"" envconfig:"APP_BASE_URL"`
TemplateDir string `default:"./templates" envconfig:"APP_TEMPLATE_DIR"`
DebugHost string `default:"0.0.0.0:4000" envconfig:"APP_DEBUG_HOST"`
ShutdownTimeout time.Duration `default:"5s" envconfig:"APP_SHUTDOWN_TIMEOUT"`
}
Redis struct {
DialTimeout time.Duration `default:"5s" envconfig:"REDIS_DIAL_TIMEOUT"`
Host string `default:":6379" envconfig:"REDIS_HOST"`
DB int `default:"1" envconfig:"REDIS_DB"`
MaxmemoryPolicy string `envconfig:"REDIS_MAXMEMORY_POLICY"`
}
DB struct {
Host string `default:"127.0.0.1:5433" envconfig:"DB_HOST"`
User string `default:"postgres" envconfig:"DB_USER"`
Pass string `default:"postgres" envconfig:"DB_PASS" json:"-"` // don't print
Database string `default:"shared" envconfig:"DB_DATABASE"`
Driver string `default:"postgres" envconfig:"DB_DRIVER"`
Timezone string `default:"utc" envconfig:"DB_TIMEZONE"`
DisableTLS bool `default:"false" envconfig:"DB_DISABLE_TLS"`
}
Trace struct {
Host string `default:"http://tracer:3002/v1/publish" envconfig:"TRACE_HOST"`
BatchSize int `default:"1000" envconfig:"TRACE_BATCH_SIZE"`
SendInterval time.Duration `default:"15s" envconfig:"TRACE_SEND_INTERVAL"`
SendTimeout time.Duration `default:"500ms" envconfig:"TRACE_SEND_TIMEOUT"`
}
AwsAccount struct {
AccessKeyID string `envconfig:"AWS_ACCESS_KEY_ID"`
SecretAccessKey string `envconfig:"AWS_SECRET_ACCESS_KEY" json:"-"` // don't print
Region string `default:"us-east-1" envconfig:"AWS_REGION"`
// Get an AWS session from an implicit source if no explicit
// configuration is provided. This is useful for taking advantage of
// EC2/ECS instance roles.
UseRole bool `envconfig:"AWS_USE_ROLE"`
}
Auth struct {
AwsSecretID string `default:"auth-secret-key" envconfig:"AUTH_AWS_SECRET_ID"`
KeyExpiration time.Duration `default:"3600s" envconfig:"AUTH_KEY_EXPIRATION"`
}
BuildInfo struct {
CiCommitRefName string `envconfig:"CI_COMMIT_REF_NAME"`
@ -74,35 +109,14 @@ func main() {
CiPipelineId string `envconfig:"CI_COMMIT_PIPELINE_ID"`
CiPipelineUrl string `envconfig:"CI_COMMIT_PIPELINE_URL"`
}
DB struct {
DialTimeout time.Duration `default:"5s" envconfig:"DB_DIAL_TIMEOUT"`
Host string `default:"mongo:27017/gotraining" envconfig:"DB_HOST"`
}
Trace struct {
Host string `default:"http://tracer:3002/v1/publish" envconfig:"TRACE_HOST"`
BatchSize int `default:"1000" envconfig:"TRACE_BATCH_SIZE"`
SendInterval time.Duration `default:"15s" envconfig:"TRACE_SEND_INTERVAL"`
SendTimeout time.Duration `default:"500ms" envconfig:"TRACE_SEND_TIMEOUT"`
}
AwsAccount struct {
AccessKeyID string `envconfig:"AWS_ACCESS_KEY_ID"`
SecretAccessKey string `envconfig:"AWS_SECRET_ACCESS_KEY"`
Region string `default:"us-east-1" envconfig:"AWS_REGION"`
// Get an AWS session from an implicit source if no explicit
// configuration is provided. This is useful for taking advantage of
// EC2/ECS instance roles.
UseRole bool `envconfig:"AWS_USE_ROLE"`
}
Auth struct {
AwsSecretID string `default:"auth-secret-key" envconfig:"AUTH_AWS_SECRET_ID"`
KeyExpiration time.Duration `default:"3600s" envconfig:"AUTH_KEY_EXPIRATION"`
}
}
// !!! This prefix seems buried, if you copy and paste this main.go
// file, its likely you will forget to update this.
if err := envconfig.Process("WEB_API", &cfg); err != nil {
// The prefix used for loading env variables.
// ie: export WEB_API_ENV=dev
envKeyPrefix := "WEB_API"
// For additional details refer to https://github.com/kelseyhightower/envconfig
if err := envconfig.Process(envKeyPrefix, &cfg); err != nil {
log.Fatalf("main : Parsing Config : %v", err)
}
@ -114,21 +128,42 @@ func main() {
}
// =========================================================================
// App Starting
// Config Validation & Defaults
// If base URL is empty, set the default value from the HTTP Host
if cfg.App.BaseUrl == "" {
baseUrl := cfg.HTTP.Host
if !strings.HasPrefix(baseUrl, "http") {
if strings.HasPrefix(baseUrl, "0.0.0.0:") {
pts := strings.Split(baseUrl, ":")
pts[0] = "127.0.0.1"
baseUrl = strings.Join(pts, ":")
} else if strings.HasPrefix(baseUrl, ":") {
baseUrl = "127.0.0.1" + baseUrl
}
baseUrl = "http://" + baseUrl
}
cfg.App.BaseUrl = baseUrl
}
// =========================================================================
// Log App Info
// 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")
// Print the config for our logs. It's important to any credentials in the config
// that could expose a security risk are excluded from being json encoded by
// applying the tag `json:"-"` to the struct var.
{
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))
}
// =========================================================================
// Init AWS Session
@ -144,6 +179,77 @@ func main() {
}
awsSession = awstrace.WrapSession(awsSession)
// =========================================================================
// Start Redis
// Ensure the eviction policy on the redis cluster is set correctly.
// AWS Elastic cache redis clusters by default have the volatile-lru.
// volatile-lru: evict keys by trying to remove the less recently used (LRU) keys first, but only among keys that have an expire set, in order to make space for the new data added.
// allkeys-lru: evict keys by trying to remove the less recently used (LRU) keys first, in order to make space for the new data added.
// Recommended to have eviction policy set to allkeys-lru
log.Println("main : Started : Initialize Redis")
redisClient := redistrace.NewClient(&redis.Options{
Addr: cfg.Redis.Host,
DB: cfg.Redis.DB,
DialTimeout: cfg.Redis.DialTimeout,
})
defer redisClient.Close()
evictPolicyConfigKey := "maxmemory-policy"
// if the maxmemory policy is set for redis, make sure its set on the cluster
// default not set and will based on the redis config values defined on the server
if cfg.Redis.MaxmemoryPolicy != "" {
err := redisClient.ConfigSet(evictPolicyConfigKey, cfg.Redis.MaxmemoryPolicy).Err()
if err != nil {
log.Fatalf("main : redis : ConfigSet maxmemory-policy : %v", err)
}
} else {
evictPolicy, err := redisClient.ConfigGet(evictPolicyConfigKey).Result()
if err != nil {
log.Fatalf("main : redis : ConfigGet maxmemory-policy : %v", err)
}
if evictPolicy[1] != "allkeys-lru" {
log.Printf("main : redis : ConfigGet maxmemory-policy : recommended to be set to allkeys-lru to avoid OOM")
}
}
// =========================================================================
// Start Database
var dbUrl url.URL
{
// Query parameters.
var q url.Values
// Handle SSL Mode
if cfg.DB.DisableTLS {
q.Set("sslmode", "disable")
} else {
q.Set("sslmode", "require")
}
q.Set("timezone", cfg.DB.Timezone)
// Construct url.
dbUrl = url.URL{
Scheme: cfg.DB.Driver,
User: url.UserPassword(cfg.DB.User, cfg.DB.Pass),
Host: cfg.DB.Host,
Path: cfg.DB.Database,
RawQuery: q.Encode(),
}
}
// Register informs the sqlxtrace package of the driver that we will be using in our program.
// It uses a default service name, in the below case "postgres.db". To use a custom service
// name use RegisterWithServiceName.
sqltrace.Register(cfg.DB.Driver, &pq.Driver{}, sqltrace.WithServiceName("my-service"))
masterDb, err := sqlxtrace.Open(cfg.DB.Driver, dbUrl.String())
if err != nil {
log.Fatalf("main : Register DB : %s : %v", cfg.DB.Driver, err)
}
defer masterDb.Close()
// =========================================================================
// Load auth keys from AWS and init new Authenticator
authenticator, err := auth.NewAuthenticator(awsSession, cfg.Auth.AwsSecretID, time.Now().UTC(), cfg.Auth.KeyExpiration)
@ -151,16 +257,6 @@ func main() {
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
@ -192,10 +288,10 @@ func main() {
//
// /debug/vars - Added to the default mux by the expvars package.
// /debug/pprof - Added to the default mux by the net/http/pprof package.
if cfg.HTTP.DebugHost != "" {
if cfg.App.DebugHost != "" {
go func() {
log.Printf("main : Debug Listening %s", cfg.HTTP.DebugHost)
log.Printf("main : Debug Listener closed : %v", http.ListenAndServe(cfg.HTTP.DebugHost, http.DefaultServeMux))
log.Printf("main : Debug Listening %s", cfg.App.DebugHost)
log.Printf("main : Debug Listener closed : %v", http.ListenAndServe(cfg.App.DebugHost, http.DefaultServeMux))
}()
}

View File

@ -30,9 +30,12 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/go-redis/redis"
"github.com/kelseyhightower/envconfig"
"github.com/lib/pq"
"go.opencensus.io/trace"
awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws"
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
redistrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
)
// build is the git version of this program. It is set using build flags in the makefile.
@ -74,6 +77,41 @@ func main() {
DebugHost string `default:"0.0.0.0:4000" envconfig:"APP_DEBUG_HOST"`
ShutdownTimeout time.Duration `default:"5s" envconfig:"APP_SHUTDOWN_TIMEOUT"`
}
Redis struct {
DialTimeout time.Duration `default:"5s" envconfig:"REDIS_DIAL_TIMEOUT"`
Host string `default:":6379" envconfig:"REDIS_HOST"`
DB int `default:"1" envconfig:"REDIS_DB"`
MaxmemoryPolicy string `envconfig:"REDIS_MAXMEMORY_POLICY"`
}
DB struct {
Host string `default:"127.0.0.1:5433" envconfig:"DB_HOST"`
User string `default:"postgres" envconfig:"DB_USER"`
Pass string `default:"postgres" envconfig:"DB_PASS" json:"-"` // don't print
Database string `default:"shared" envconfig:"DB_DATABASE"`
Driver string `default:"postgres" envconfig:"DB_DRIVER"`
Timezone string `default:"utc" envconfig:"DB_TIMEZONE"`
DisableTLS bool `default:"false" envconfig:"DB_DISABLE_TLS"`
}
Trace struct {
Host string `default:"http://tracer:3002/v1/publish" envconfig:"TRACE_HOST"`
BatchSize int `default:"1000" envconfig:"TRACE_BATCH_SIZE"`
SendInterval time.Duration `default:"15s" envconfig:"TRACE_SEND_INTERVAL"`
SendTimeout time.Duration `default:"500ms" envconfig:"TRACE_SEND_TIMEOUT"`
}
AwsAccount struct {
AccessKeyID string `envconfig:"AWS_ACCESS_KEY_ID"`
SecretAccessKey string `envconfig:"AWS_SECRET_ACCESS_KEY" json:"-"` // don't print
Region string `default:"us-east-1" envconfig:"AWS_REGION"`
// Get an AWS session from an implicit source if no explicit
// configuration is provided. This is useful for taking advantage of
// EC2/ECS instance roles.
UseRole bool `envconfig:"AWS_USE_ROLE"`
}
Auth struct {
AwsSecretID string `default:"auth-secret-key" envconfig:"AUTH_AWS_SECRET_ID"`
KeyExpiration time.Duration `default:"3600s" envconfig:"AUTH_KEY_EXPIRATION"`
}
BuildInfo struct {
CiCommitRefName string `envconfig:"CI_COMMIT_REF_NAME"`
CiCommitRefSlug string `envconfig:"CI_COMMIT_REF_SLUG"`
@ -86,42 +124,15 @@ func main() {
CiPipelineId string `envconfig:"CI_COMMIT_PIPELINE_ID"`
CiPipelineUrl string `envconfig:"CI_COMMIT_PIPELINE_URL"`
}
Redis struct {
DialTimeout time.Duration `default:"5s" envconfig:"REDIS_DIAL_TIMEOUT"`
Host string `default:":6379" envconfig:"REDIS_HOST"`
DB int `default:"1" envconfig:"REDIS_DB"`
MaxmemoryPolicy string `envconfig:"REDIS_MAXMEMORY_POLICY"`
}
DB struct {
DialTimeout time.Duration `default:"5s" envconfig:"DB_DIAL_TIMEOUT"`
Host string `default:"mongo:27017/gotraining" envconfig:"DB_HOST"`
}
Trace struct {
Host string `default:"http://tracer:3002/v1/publish" envconfig:"TRACE_HOST"`
BatchSize int `default:"1000" envconfig:"TRACE_BATCH_SIZE"`
SendInterval time.Duration `default:"15s" envconfig:"TRACE_SEND_INTERVAL"`
SendTimeout time.Duration `default:"500ms" envconfig:"TRACE_SEND_TIMEOUT"`
}
AwsAccount struct {
AccessKeyID string `envconfig:"AWS_ACCESS_KEY_ID"`
SecretAccessKey string `envconfig:"AWS_SECRET_ACCESS_KEY"`
Region string `default:"us-east-1" envconfig:"AWS_REGION"`
// Get an AWS session from an implicit source if no explicit
// configuration is provided. This is useful for taking advantage of
// EC2/ECS instance roles.
UseRole bool `envconfig:"AWS_USE_ROLE"`
}
Auth struct {
AwsSecretID string `default:"auth-secret-key" envconfig:"AUTH_AWS_SECRET_ID"`
KeyExpiration time.Duration `default:"3600s" envconfig:"AUTH_KEY_EXPIRATION"`
}
CMD string `envconfig:"CMD"`
}
// !!! This prefix seems buried, if you copy and paste this main.go
// file, its likely you will forget to update this.
if err := envconfig.Process("WEB_APP", &cfg); err != nil {
// The prefix used for loading env variables.
// ie: export WEB_APP_ENV=dev
envKeyPrefix := "WEB_APP"
// For additional details refer to https://github.com/kelseyhightower/envconfig
if err := envconfig.Process(envKeyPrefix, &cfg); err != nil {
log.Fatalf("main : Parsing Config : %v", err)
}
@ -132,6 +143,9 @@ func main() {
return // We displayed help.
}
// =========================================================================
// Config Validation & Defaults
// If base URL is empty, set the default value from the HTTP Host
if cfg.App.BaseUrl == "" {
baseUrl := cfg.HTTP.Host
@ -149,17 +163,23 @@ func main() {
}
// =========================================================================
// App Starting
// Log App Info
// 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")
// Print the config for our logs. It's important to any credentials in the config
// that could expose a security risk are excluded from being json encoded by
// applying the tag `json:"-"` to the struct var.
{
cfgJSON, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Fatalf("main : Marshalling Config to JSON : %v", err)
}
log.Printf("main : Config : %v\n", string(cfgJSON))
}
// TODO: Validate what is being written to the logs. We don't
// want to leak credentials or anything that can be a security risk.
@ -199,7 +219,7 @@ func main() {
// if the maxmemory policy is set for redis, make sure its set on the cluster
// default not set and will based on the redis config values defined on the server
if cfg.Redis.MaxmemoryPolicy != "" {
err = redisClient.ConfigSet(evictPolicyConfigKey, cfg.Redis.MaxmemoryPolicy).Err()
err := redisClient.ConfigSet(evictPolicyConfigKey, cfg.Redis.MaxmemoryPolicy).Err()
if err != nil {
log.Fatalf("main : redis : ConfigSet maxmemory-policy : %v", err)
}
@ -214,6 +234,42 @@ func main() {
}
}
// =========================================================================
// Start Database
var dbUrl url.URL
{
// Query parameters.
var q url.Values
// Handle SSL Mode
if cfg.DB.DisableTLS {
q.Set("sslmode", "disable")
} else {
q.Set("sslmode", "require")
}
q.Set("timezone", cfg.DB.Timezone)
// Construct url.
dbUrl = url.URL{
Scheme: cfg.DB.Driver,
User: url.UserPassword(cfg.DB.User, cfg.DB.Pass),
Host: cfg.DB.Host,
Path: cfg.DB.Database,
RawQuery: q.Encode(),
}
}
// Register informs the sqlxtrace package of the driver that we will be using in our program.
// It uses a default service name, in the below case "postgres.db". To use a custom service
// name use RegisterWithServiceName.
sqltrace.Register(cfg.DB.Driver, &pq.Driver{}, sqltrace.WithServiceName("my-service"))
masterDb, err := sqlxtrace.Open(cfg.DB.Driver, dbUrl.String())
if err != nil {
log.Fatalf("main : Register DB : %s : %v", cfg.DB.Driver, err)
}
defer masterDb.Close()
// =========================================================================
// Deploy
switch cfg.CMD {
@ -494,7 +550,7 @@ func main() {
api := http.Server{
Addr: cfg.HTTP.Host,
Handler: handlers.APP(shutdown, log, cfg.App.StaticDir, cfg.App.TemplateDir, nil, nil, renderer),
Handler: handlers.APP(shutdown, log, cfg.App.StaticDir, cfg.App.TemplateDir, masterDb, nil, renderer),
ReadTimeout: cfg.HTTP.ReadTimeout,
WriteTimeout: cfg.HTTP.WriteTimeout,
MaxHeaderBytes: 1 << 20,

View File

@ -4,63 +4,117 @@
# docker-compose down
version: '3'
networks:
shared-network:
driver: bridge
services:
# This starts a local mongo DB.
mongo:
container_name: mongo
networks:
- shared-network
image: mongo:3-jessie
postgres:
container_name: postgres
image: postgres:11-alpine
expose:
- "5433"
ports:
- 27017:27017
command: --bind_ip 0.0.0.0
# This is the core CRUD based service.
web-api:
container_name: web-api
networks:
- shared-network
image: gcr.io/web-api/web-api-amd64:1.0
ports:
- 3000:3000 # CRUD API
- 4000:4000 # DEBUG API
- "5433:5432"
environment:
- WEB_APP_AUTH_KEY_ID=1
# - WEB_APP_DB_HOST=got:got2015@ds039441.mongolab.com:39441/gotraining
- POSTGRES_USER=postgres
- POSTGRES_PASS=postgres
- POSTGRES_DB=shared
redis:
image: redis:latest
expose:
- "6379"
ports:
- "6379:6379"
entrypoint: redis-server --appendonly yes
datadog:
container_name: datadog
build:
context: .
dockerfile: docker/datadog-agent/Dockerfile
image: example-project/datadog:latest
ports:
- 8125:8125
- 8126:8126
env_file:
- .env_docker_compose
environment:
- DD_API_KEY=${DD_API_KEY}
- DD_LOGS_ENABLED=true
- DD_APM_ENABLED=true
- DD_RECEIVER_PORT=8126
- DD_APM_NON_LOCAL_TRAFFIC=true
- DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL=true
- DD_TAGS=source:docker env:dev
- DD_DOGSTATSD_ORIGIN_DETECTION=true
- DD_DOGSTATSD_NON_LOCAL_TRAFFIC=true
- ECS_FARGATE=false
web-app:
container_name: web-app
build:
context: .
dockerfile: cmd/web-app/Dockerfile
image: example-project/web-api:latest
ports:
- 3000:3000 # WEB APP
- 4000:4000 # DEBUG API
links:
- postgres
- redis
- datadog
env_file:
- .env_docker_compose
environment:
- WEB_APP_HTTP_HOST=0.0.0.0:3000
- WEB_APP_APP_BASE_URL=http://127.0.0.1:3000
- WEB_APP_REDIS_HOST=redis
- WEB_APP_DB_HOST=postgres
- WEB_APP_DB_USER=postgres
- WEB_APP_DB_PASS=postgres
- WEB_APP_DB_DATABASE=shared
- WEB_APP_AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- WEB_APP_AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- WEB_APP_AWS_REGION=${AWS_REGION}
- WEB_APP_AWS_USE_ROLE=${AWS_USE_ROLE}
- DD_TRACE_AGENT_HOSTNAME=datadog
- DD_TRACE_AGENT_PORT=8126
- DD_SERVICE_NAME=web-app
- DD_ENV=dev
# - GODEBUG=gctrace=1
# This sidecar publishes metrics to the console by default.
metrics:
container_name: metrics
networks:
- shared-network
image: gcr.io/web-api/metrics-amd64:1.0
web-api:
container_name: web-api
build:
context: .
dockerfile: cmd/web-api/Dockerfile
image: example-project/web-api:latest
ports:
- 3001:3001 # EXPVAR API
- 3001:3001 # WEB API
- 4001:4001 # DEBUG API
links:
- postgres
- redis
- datadog
env_file:
- .env_docker_compose
environment:
- WEB_API_HTTP_HOST=0.0.0.0:3001
- WEB_API_APP_BASE_URL=http://127.0.0.1:3001
- WEB_API_REDIS_HOST=redis
- WEB_API_DB_HOST=postgres
- WEB_API_DB_USER=postgres
- WEB_API_DB_PASS=postgres
- WEB_API_DB_DATABASE=shared
- WEB_API_AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- WEB_API_AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- WEB_API_AWS_REGION=${AWS_REGION}
- WEB_API_AWS_USE_ROLE=${AWS_USE_ROLE}
- DD_TRACE_AGENT_HOSTNAME=datadog
- DD_TRACE_AGENT_PORT=8126
- DD_SERVICE_NAME=web-app
- DD_ENV=dev
# - GODEBUG=gctrace=1
# This sidecar publishes tracing to the console by default.
tracer:
container_name: tracer
networks:
- shared-network
image: gcr.io/web-api/tracer-amd64:1.0
ports:
- 3002:3002 # TRACER API
- 4002:4002 # DEBUG API
# environment:
# - WEB_APP_ZIPKIN_HOST=http://zipkin:9411/api/v2/spans
# This sidecar allows for the viewing of traces.
zipkin:
container_name: zipkin
networks:
- shared-network
image: openzipkin/zipkin:2.11
ports:
- 9411:9411

View File

@ -0,0 +1,16 @@
FROM datadog/agent:latest
LABEL maintainer="lee@geeksinthewoods.com"
#COPY target/go_expvar.conf.yaml /etc/datadog-agent/conf.d/go_expvar.d/conf.yaml
COPY target/custom-init.sh /custom-init.sh
ARG service
ENV SERVICE_NAME $service
ARG env="dev"
ENV ENV $env
ENV DD_TAGS="source:docker service:${service} service_name:${service} cluster:NA env:${ENV}"
CMD ["/custom-init.sh"]

View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
configFile="/etc/datadog-agent/conf.d/go_expvar.d/conf.yaml"
echo -e "init_config:\n\ninstances:\n - expvar_url: http://localhost:80/debug/vars" > $configFile
if [[ "${DD_TAGS}" != "" ]]; then
echo " tags:" >> $configFile
for t in ${DD_TAGS}; do
echo " - \"${t}\"" >> $configFile
done
fi
cat $configFile
/init

View File

@ -1,33 +1,35 @@
module geeks-accelerator/oss/saas-starter-kit/example-project
require (
github.com/GuiaBolso/darwin v0.0.0-20170210191649-86919dfcf808 // indirect
github.com/aws/aws-sdk-go v1.19.33
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dimfeld/httptreemux v5.0.1+incompatible
github.com/gitwak/gondolier v0.0.0-20190521205431-504d297a6c42 // indirect
github.com/gitwak/sqlxmigrate v0.0.0-20190522211042-9625063dea5d
github.com/go-playground/locales v0.12.1
github.com/go-playground/universal-translator v0.16.0
github.com/go-redis/redis v6.15.2+incompatible
github.com/google/go-cmp v0.2.0
github.com/hashicorp/golang-lru v0.5.1
github.com/jmoiron/sqlx v1.2.0
github.com/kelseyhightower/envconfig v1.3.0
github.com/kr/pretty v0.1.0 // indirect
github.com/leodido/go-urn v1.1.0 // indirect
github.com/lib/pq v1.1.1
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/opentracing/opentracing-go v1.1.0 // indirect
github.com/openzipkin/zipkin-go v0.1.1
github.com/pborman/uuid v0.0.0-20180122190007-c65b2f87fee3
github.com/philhofer/fwd v1.0.0 // indirect
github.com/philippgille/gokv v0.5.0 // indirect
github.com/pkg/errors v0.8.0
github.com/pkg/errors v0.8.1
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0
github.com/stretchr/testify v1.3.0 // indirect
github.com/tinylib/msgp v1.1.0 // indirect
go.opencensus.io v0.14.0
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 // indirect
golang.org/x/sys v0.0.0-20190516110030-61b9204099cb // indirect
golang.org/x/text v0.3.0 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.13.1
gopkg.in/DataDog/dd-trace-go.v1 v1.14.0
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v9 v9.28.0

View File

@ -1,3 +1,5 @@
github.com/GuiaBolso/darwin v0.0.0-20170210191649-86919dfcf808 h1:rxDa2t7Ep7E26WMVHjl+mdLr9Un7yRSzz1CwRW6fWNY=
github.com/GuiaBolso/darwin v0.0.0-20170210191649-86919dfcf808/go.mod h1:3sqgkckuISJ5rs1EpOp6vCvwOUKe/z9vPmyuIlq8Q/A=
github.com/aws/aws-sdk-go v1.19.32 h1:/usjSR6qsKfOKzk4tDNvZq7LqmP5+J0Cq/Uwsr2XVG8=
github.com/aws/aws-sdk-go v1.19.32/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.19.33 h1:qz9ZQtxCUuwBKdc5QiY6hKuISYGeRQyLVA2RryDEDaQ=
@ -8,18 +10,28 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dimfeld/httptreemux v5.0.1+incompatible h1:Qj3gVcDNoOthBAqftuD596rm4wg/adLLz5xh5CmpiCA=
github.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0=
github.com/gitwak/gondolier v0.0.0-20190521205431-504d297a6c42 h1:+lo4HFeG6LlcgwvsvQC8H5FG8yr/kDn89E51BTw3loE=
github.com/gitwak/gondolier v0.0.0-20190521205431-504d297a6c42/go.mod h1:ecEQ8e4eHeWKPf+g6ByatPM7l4QZgR3G5ZIZKvEAdCE=
github.com/gitwak/sqlxmigrate v0.0.0-20190522211042-9625063dea5d h1:oaUPMY0F+lNUkyB5tzsQS3EC0m9Cxdglesp63i3UPso=
github.com/gitwak/sqlxmigrate v0.0.0-20190522211042-9625063dea5d/go.mod h1:e7vYkZWKUHC2Vl0/dIiQRKR3z2HMuswoLf2IiQmnMoQ=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4=
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -29,6 +41,12 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
@ -43,6 +61,8 @@ github.com/philippgille/gokv v0.5.0 h1:6bgvKt+RR1BDxhD/oLXDTA9a7ws8xbgV3767ytBNr
github.com/philippgille/gokv v0.5.0/go.mod h1:3qSKa2SgG4qXwLfF4htVEWRoRNLi86+fNdn+jQH5Clw=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 h1:X9XMOYjxEfAYSy3xK1DzO5dMkkWhs9E9UCcS1IERx2k=
@ -62,8 +82,11 @@ golang.org/x/sys v0.0.0-20190516110030-61b9204099cb h1:k07iPOt0d6nEnwXF+kHB+iEg+
golang.org/x/sys v0.0.0-20190516110030-61b9204099cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/DataDog/dd-trace-go.v1 v1.13.1 h1:oTzOClfuudNhW9Skkp2jxjqYO92uDKXqKLbiuPA13Rk=
gopkg.in/DataDog/dd-trace-go.v1 v1.13.1/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
gopkg.in/DataDog/dd-trace-go.v1 v1.14.0 h1:p/8j8WV6HC+6c99FMWIPrPPs+PiXU/ShrBxHbO8S8V0=
gopkg.in/DataDog/dd-trace-go.v1 v1.14.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -1,124 +0,0 @@
// All material is licensed under the Apache License Version 2.0, January 2004
// http://www.apache.org/licenses/LICENSE-2.0
package db
import (
"context"
"encoding/json"
"time"
"github.com/pkg/errors"
"go.opencensus.io/trace"
mgo "gopkg.in/mgo.v2"
)
// ErrInvalidDBProvided is returned in the event that an uninitialized db is
// used to perform actions against.
var ErrInvalidDBProvided = errors.New("invalid DB provided")
// DB is a collection of support for different DB technologies. Currently
// only MongoDB has been implemented. We want to be able to access the raw
// database support for the given DB so an interface does not work. Each
// database is too different.
type DB struct {
// MongoDB Support.
database *mgo.Database
session *mgo.Session
}
// New returns a new DB value for use with MongoDB based on a registered
// master session.
func New(url string, timeout time.Duration) (*DB, error) {
// Set the default timeout for the session.
if timeout == 0 {
timeout = 60 * time.Second
}
// Create a session which maintains a pool of socket connections
// to our MongoDB.
ses, err := mgo.DialWithTimeout(url, timeout)
if err != nil {
return nil, errors.Wrapf(err, "mgo.DialWithTimeout: %s,%v", url, timeout)
}
// Reads may not be entirely up-to-date, but they will always see the
// history of changes moving forward, the data read will be consistent
// across sequential queries in the same session, and modifications made
// within the session will be observed in following queries (read-your-writes).
// http://godoc.org/labix.org/v2/mgo#Session.SetMode
ses.SetMode(mgo.Monotonic, true)
db := DB{
database: ses.DB(""),
session: ses,
}
return &db, nil
}
// Close closes a DB value being used with MongoDB.
func (db *DB) Close() {
db.session.Close()
}
// Copy returns a new DB value for use with MongoDB based on master session.
func (db *DB) Copy() *DB {
ses := db.session.Copy()
// As per the mgo documentation, https://godoc.org/gopkg.in/mgo.v2#Session.DB
// if no database name is specified, then use the default one, or the one that
// the connection was dialed with.
newDB := DB{
database: ses.DB(""),
session: ses,
}
return &newDB
}
// Execute is used to execute MongoDB commands.
func (db *DB) Execute(ctx context.Context, collName string, f func(*mgo.Collection) error) error {
ctx, span := trace.StartSpan(ctx, "platform.DB.Execute")
defer span.End()
if db == nil || db.session == nil {
return errors.Wrap(ErrInvalidDBProvided, "db == nil || db.session == nil")
}
return f(db.database.C(collName))
}
// ExecuteTimeout is used to execute MongoDB commands with a timeout.
func (db *DB) ExecuteTimeout(ctx context.Context, timeout time.Duration, collName string, f func(*mgo.Collection) error) error {
ctx, span := trace.StartSpan(ctx, "platform.DB.ExecuteTimeout")
defer span.End()
if db == nil || db.session == nil {
return errors.Wrap(ErrInvalidDBProvided, "db == nil || db.session == nil")
}
db.session.SetSocketTimeout(timeout)
return f(db.database.C(collName))
}
// StatusCheck validates the DB status good.
func (db *DB) StatusCheck(ctx context.Context) error {
ctx, span := trace.StartSpan(ctx, "platform.DB.StatusCheck")
defer span.End()
return nil
}
// Query provides a string version of the value
func Query(value interface{}) string {
json, err := json.Marshal(value)
if err != nil {
return ""
}
return string(json)
}

View File

@ -0,0 +1,5 @@
AWS_ACCESS_KEY_ID=XXXX
AWS_SECRET_ACCESS_KEY=XXXX
AWS_REGION=us-east-1
AWS_USE_ROLE=false
DD_API_KEY=XXXX

View File

@ -0,0 +1,17 @@
package main
import (
"github.com/jmoiron/sqlx"
"log"
)
// initSchema runs before any migrations are executed. This happens when no other migrations
// have previously been executed.
func initSchema(db *sqlx.DB, log *log.Logger) func(*sqlx.DB) error {
f := func(*sqlx.DB) error {
return nil
}
return f
}

View File

@ -0,0 +1,130 @@
package main
import (
"encoding/json"
"expvar"
"github.com/lib/pq"
"log"
"net/url"
"os"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
"github.com/kelseyhightower/envconfig"
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
"github.com/gitwak/sqlxmigrate"
_ "github.com/lib/pq"
)
// 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, "Schema : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
// =========================================================================
// Configuration
var cfg struct {
Env string `default:"dev" envconfig:"ENV"`
DB struct {
Host string `default:"127.0.0.1:5433" envconfig:"DB_HOST"`
User string `default:"postgres" envconfig:"DB_USER"`
Pass string `default:"postgres" envconfig:"DB_PASS" json:"-"` // don't print
Database string `default:"shared" envconfig:"DB_DATABASE"`
Driver string `default:"postgres" envconfig:"DB_DRIVER"`
Timezone string `default:"utc" envconfig:"DB_TIMEZONE"`
DisableTLS bool `default:"false" envconfig:"DB_DISABLE_TLS"`
}
}
// The prefix used for loading env variables.
// ie: export SCHEMA_ENV=dev
envKeyPrefix := "SCHEMA"
// For additional details refer to https://github.com/kelseyhightower/envconfig
if err := envconfig.Process(envKeyPrefix, &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.
}
// =========================================================================
// Log App Info
// 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")
// Print the config for our logs. It's important to any credentials in the config
// that could expose a security risk are excluded from being json encoded by
// applying the tag `json:"-"` to the struct var.
{
cfgJSON, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Fatalf("main : Marshalling Config to JSON : %v", err)
}
log.Printf("main : Config : %v\n", string(cfgJSON))
}
// =========================================================================
// Start Database
var dbUrl url.URL
{
// Query parameters.
var q url.Values
// Handle SSL Mode
if cfg.DB.DisableTLS {
q.Set("sslmode", "disable")
} else {
q.Set("sslmode", "require")
}
q.Set("timezone", cfg.DB.Timezone)
// Construct url.
dbUrl = url.URL{
Scheme: cfg.DB.Driver,
User: url.UserPassword(cfg.DB.User, cfg.DB.Pass),
Host: cfg.DB.Host,
Path: cfg.DB.Database,
RawQuery: q.Encode(),
}
}
// Register informs the sqlxtrace package of the driver that we will be using in our program.
// It uses a default service name, in the below case "postgres.db". To use a custom service
// name use RegisterWithServiceName.
sqltrace.Register(cfg.DB.Driver, &pq.Driver{}, sqltrace.WithServiceName("my-service"))
masterDb, err := sqlxtrace.Open(cfg.DB.Driver, dbUrl.String())
if err != nil {
log.Fatalf("main : Register DB : %s : %v", cfg.DB.Driver, err)
}
defer masterDb.Close()
// =========================================================================
// Start Migrations
// Load list of Schema migrations and init new sqlxmigrate client
migrations := migrationList(masterDb, log)
m := sqlxmigrate.New(masterDb, sqlxmigrate.DefaultOptions, migrations)
// Append any schema that need to be applied if this is a fresh migration
// ie. the migrations database table does not exist.
m.InitSchema(initSchema(masterDb, log))
// Execute the migrations
if err = m.Migrate(); err != nil {
log.Fatalf("main : Migrate : %v", err)
}
log.Printf("main : Migrate : Completed")
}

View File

@ -0,0 +1,129 @@
package main
import (
"database/sql"
"log"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/gitwak/sqlxmigrate"
_ "github.com/lib/pq"
)
// migrationList returns a list of migrations to be executed. If the id of the
// migration already exists in the migrations table it will be skipped.
func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
return []*sqlxmigrate.Migration{
// create table users
{
ID: "20190522-01a",
Migrate: func(tx *sql.Tx) error {
q := `CREATE TABLE IF NOT EXISTS users (
id char(36) NOT NULL,
email varchar(200) NOT NULL,
title varchar(100) NOT NULL DEFAULT '',
first_name varchar(200) NOT NULL DEFAULT '',
last_name varchar(200) NOT NULL DEFAULT '',
password_hash varchar(200) NOT NULL,
password_reset varchar(200) DEFAULT NULL,
password_salt varchar(200) NOT NULL,
phone varchar(20) NOT NULL DEFAULT '',
status enum('active','disabled') NOT NULL DEFAULT 'active',
timezone varchar(128) NOT NULL DEFAULT 'America/Anchorage',
created_at timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp(0) DEFAULT NULL,
deleted_at timestamp(0) DEFAULT NULL,
PRIMARY KEY (id),
CONSTRAINT email UNIQUE (email)
) ;`
if _, err := tx.Exec(q); err != nil {
return errors.WithMessagef(err, "Query failed %s", q)
}
return nil
},
Rollback: func(tx *sql.Tx) error {
q := `DROP TABLE IF EXISTS users`
if _, err := tx.Exec(q); err != nil {
return errors.WithMessagef(err, "Query failed %s", q)
}
return nil
},
},
// create new table accounts
{
ID: "20190522-01b",
Migrate: func(tx *sql.Tx) error {
q := `CREATE TABLE IF NOT EXISTS accounts (
id char(36) NOT NULL,
name varchar(255) NOT NULL,
address1 varchar(255) NOT NULL DEFAULT '',
address2 varchar(255) NOT NULL DEFAULT '',
city varchar(100) NOT NULL DEFAULT '',
region varchar(255) NOT NULL DEFAULT '',
country varchar(255) NOT NULL DEFAULT '',
zipcode varchar(20) NOT NULL DEFAULT '',
status enum('active','pending','disabled') NOT NULL DEFAULT 'active',
timezone varchar(128) NOT NULL DEFAULT 'America/Anchorage',
signup_user_id char(36) DEFAULT NULL,
billing_user_id char(36) DEFAULT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT NULL,
deleted_at datetime DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`
if _, err := tx.Exec(q); err != nil {
return errors.WithMessagef(err, "Query failed %s", q)
}
return nil
},
Rollback: func(tx *sql.Tx) error {
q := `DROP TABLE IF EXISTS accounts`
if _, err := tx.Exec(q); err != nil {
return errors.WithMessagef(err, "Query failed %s", q)
}
return nil
},
},
// create new table user_accounts
{
ID: "20190522-01c",
Migrate: func(tx *sql.Tx) error {
q1 := `CREATE TYPE IF NOT EXISTS role_t as enum('admin', 'user');`
if _, err := tx.Exec(q1); err != nil {
return errors.WithMessagef(err, "Query failed %s", q1)
}
q2 := `CREATE TABLE IF NOT EXISTS users_accounts (
id char(36) NOT NULL,
account_id char(36) NOT NULL,
user_id ichar(36) NOT NULL,
roles role_t[] NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT NULL,
deleted_at datetime DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY user_account (user_id,account_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`
if _, err := tx.Exec(q1); err != nil {
return errors.WithMessagef(err, "Query failed %s", q2)
}
return nil
},
Rollback: func(tx *sql.Tx) error {
q1 := `DROP TYPE IF EXISTS role_t`
if _, err := tx.Exec(q1); err != nil {
return errors.WithMessagef(err, "Query failed %s", q1)
}
q2 := `DROP TABLE IF EXISTS users_accounts`
if _, err := tx.Exec(q2); err != nil {
return errors.WithMessagef(err, "Query failed %s", q2)
}
return nil
},
},
}
}