2019-05-16 10:39:25 -04:00
package main
import (
"context"
2019-07-11 14:15:29 -08:00
"crypto/tls"
2019-05-16 10:39:25 -04:00
"encoding/json"
"expvar"
2019-05-23 19:40:29 -05:00
"fmt"
2019-05-16 10:39:25 -04:00
"log"
2019-07-12 11:41:41 -08:00
"net"
2019-05-16 10:39:25 -04:00
"net/http"
_ "net/http/pprof"
2019-05-23 14:01:24 -05:00
"net/url"
2019-05-16 10:39:25 -04:00
"os"
"os/signal"
2019-07-12 23:28:53 -08:00
"path/filepath"
2019-05-23 14:01:24 -05:00
"strings"
2019-05-16 10:39:25 -04:00
"syscall"
"time"
2019-07-13 12:16:28 -08:00
"geeks-accelerator/oss/saas-starter-kit/cmd/web-api/docs"
"geeks-accelerator/oss/saas-starter-kit/cmd/web-api/handlers"
2019-08-14 12:53:40 -08:00
"geeks-accelerator/oss/saas-starter-kit/internal/account"
"geeks-accelerator/oss/saas-starter-kit/internal/account/account_preference"
2019-07-13 12:16:28 -08:00
"geeks-accelerator/oss/saas-starter-kit/internal/mid"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/devops"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/flag"
2019-08-14 12:53:40 -08:00
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/project"
"geeks-accelerator/oss/saas-starter-kit/internal/project_route"
"geeks-accelerator/oss/saas-starter-kit/internal/signup"
"geeks-accelerator/oss/saas-starter-kit/internal/user"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account/invite"
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
2019-05-17 14:42:50 -04:00
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
2019-07-14 14:55:34 -08:00
"github.com/aws/aws-sdk-go/aws/ec2metadata"
2019-05-17 14:42:50 -04:00
"github.com/aws/aws-sdk-go/aws/session"
2019-05-23 14:32:24 -05:00
"github.com/go-redis/redis"
2019-08-14 12:53:40 -08:00
"github.com/gorilla/securecookie"
2019-05-16 10:39:25 -04:00
"github.com/kelseyhightower/envconfig"
2019-05-23 14:32:24 -05:00
"github.com/lib/pq"
2019-08-14 12:53:40 -08:00
"github.com/pkg/errors"
2019-07-13 03:03:30 -08:00
"golang.org/x/crypto/acme"
2019-07-11 14:46:05 -08:00
"golang.org/x/crypto/acme/autocert"
2019-05-17 14:42:50 -04:00
awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws"
2019-05-23 14:01:24 -05:00
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"
2019-06-24 17:36:42 -08:00
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
2019-05-16 10:39:25 -04:00
)
// build is the git version of this program. It is set using build flags in the makefile.
var build = "develop"
2019-06-24 01:30:18 -08:00
// service is the name of the program used for logging, tracing and the
// the prefix used for loading env variables
// ie: export WEB_API_ENV=dev
var service = "WEB_API"
2019-06-24 17:36:42 -08:00
// @title SaaS Example API
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
2019-06-25 06:25:55 -08:00
// @securityDefinitions.basic BasicAuth
2019-06-24 17:36:42 -08:00
// @securitydefinitions.oauth2.password OAuth2Password
2019-06-24 22:41:21 -08:00
// @tokenUrl /v1/oauth/token
2019-06-25 22:31:54 -08:00
// @scope.user Grants basic privileges with role of user.
// @scope.admin Grants administrative privileges with role of admin.
2019-06-24 17:36:42 -08:00
2019-05-16 10:39:25 -04:00
func main ( ) {
// =========================================================================
// Logging
2019-08-14 12:53:40 -08:00
log . SetFlags ( log . LstdFlags | log . Lmicroseconds | log . Lshortfile )
log . SetPrefix ( service + " : " )
log := log . New ( os . Stdout , log . Prefix ( ) , log . Flags ( ) )
2019-05-16 10:39:25 -04:00
// =========================================================================
// Configuration
var cfg struct {
2019-05-20 22:16:58 -05:00
Env string ` default:"dev" envconfig:"ENV" `
2019-05-18 18:06:10 -04:00
HTTP struct {
2019-07-14 17:03:08 -08:00
Host string ` default:"0.0.0.0:3001" envconfig:"HOST" `
2019-05-23 19:40:29 -05:00
ReadTimeout time . Duration ` default:"10s" envconfig:"READ_TIMEOUT" `
WriteTimeout time . Duration ` default:"10s" envconfig:"WRITE_TIMEOUT" `
2019-05-23 14:01:24 -05:00
}
HTTPS struct {
2019-05-23 19:40:29 -05:00
Host string ` default:"" envconfig:"HOST" `
ReadTimeout time . Duration ` default:"5s" envconfig:"READ_TIMEOUT" `
WriteTimeout time . Duration ` default:"5s" envconfig:"WRITE_TIMEOUT" `
2019-07-13 03:03:30 -08:00
DisableHTTP2 bool ` default:"false" envconfig:"DISABLE_HTTP2" `
2019-05-20 22:16:58 -05:00
}
2019-07-13 12:16:28 -08:00
Service struct {
2019-08-14 12:53:40 -08:00
Name string ` default:"web-api" envconfig:"SERVICE" `
2019-08-13 16:06:11 -08:00
BaseUrl string ` default:"" envconfig:"BASE_URL" example:"http://api.example.saasstartupkit.com" `
HostNames [ ] string ` envconfig:"HOST_NAMES" example:"alternative-subdomain.example.saasstartupkit.com" `
2019-07-14 00:41:58 -08:00
EnableHTTPS bool ` default:"false" envconfig:"ENABLE_HTTPS" `
2019-05-23 19:40:29 -05:00
TemplateDir string ` default:"./templates" envconfig:"TEMPLATE_DIR" `
DebugHost string ` default:"0.0.0.0:4000" envconfig:"DEBUG_HOST" `
ShutdownTimeout time . Duration ` default:"5s" envconfig:"SHUTDOWN_TIMEOUT" `
2019-05-20 22:16:58 -05:00
}
2019-08-14 12:53:40 -08:00
Project struct {
Name string ` default:"" envconfig:"PROJECT" `
SharedTemplateDir string ` default:"../../resources/templates/shared" envconfig:"SHARED_TEMPLATE_DIR" `
SharedSecretKey string ` default:"" envconfig:"SHARED_SECRET_KEY" `
EmailSender string ` default:"test@example.saasstartupkit.com" envconfig:"EMAIL_SENDER" `
WebAppBaseUrl string ` default:"http://127.0.0.1:3000" envconfig:"WEB_APP_BASE_URL" example:"www.example.saasstartupkit.com" `
}
2019-05-23 14:01:24 -05:00
Redis struct {
2019-05-23 19:40:29 -05:00
Host string ` default:":6379" envconfig:"HOST" `
DB int ` default:"1" envconfig:"DB" `
DialTimeout time . Duration ` default:"5s" envconfig:"DIAL_TIMEOUT" `
MaxmemoryPolicy string ` envconfig:"MAXMEMORY_POLICY" `
2019-05-16 10:39:25 -04:00
}
DB struct {
2019-05-23 19:40:29 -05:00
Host string ` default:"127.0.0.1:5433" envconfig:"HOST" `
User string ` default:"postgres" envconfig:"USER" `
Pass string ` default:"postgres" envconfig:"PASS" json:"-" ` // don't print
Database string ` default:"shared" envconfig:"DATABASE" `
Driver string ` default:"postgres" envconfig:"DRIVER" `
Timezone string ` default:"utc" envconfig:"TIMEZONE" `
2019-06-24 22:41:21 -08:00
DisableTLS bool ` default:"true" envconfig:"DISABLE_TLS" `
2019-05-16 10:39:25 -04:00
}
Trace struct {
2019-05-25 08:26:37 -05:00
Host string ` default:"127.0.0.1" envconfig:"DD_TRACE_AGENT_HOSTNAME" `
Port int ` default:"8126" envconfig:"DD_TRACE_AGENT_PORT" `
AnalyticsRate float64 ` default:"0.10" envconfig:"ANALYTICS_RATE" `
2019-05-16 10:39:25 -04:00
}
2019-05-23 19:40:29 -05:00
Aws struct {
2019-07-13 03:03:30 -08:00
AccessKeyID string ` envconfig:"AWS_ACCESS_KEY_ID" ` // WEB_API_AWS_AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY_ID
SecretAccessKey string ` envconfig:"AWS_SECRET_ACCESS_KEY" json:"-" ` // don't print
2019-07-14 01:07:14 -08:00
Region string ` default:"us-west-2" envconfig:"AWS_REGION" `
2019-07-13 03:03:30 -08:00
S3BucketPrivate string ` envconfig:"S3_BUCKET_PRIVATE" `
S3BucketPublic string ` envconfig:"S3_BUCKET_PUBLIC" `
SecretsManagerConfigPrefix string ` default:"" envconfig:"SECRETS_MANAGER_CONFIG_PREFIX" `
2019-05-17 14:42:50 -04:00
// 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" `
2019-05-16 10:39:25 -04:00
}
Auth struct {
2019-07-11 00:58:45 -08:00
UseAwsSecretManager bool ` default:"false" envconfig:"USE_AWS_SECRET_MANAGER" `
2019-06-24 17:36:42 -08:00
KeyExpiration time . Duration ` default:"3600s" envconfig:"KEY_EXPIRATION" `
2019-05-16 10:39:25 -04:00
}
2019-05-23 14:01:24 -05:00
BuildInfo struct {
2019-08-06 19:45:29 -08:00
CiCommitRefName string ` envconfig:"CI_COMMIT_REF_NAME" `
CiCommitShortSha string ` envconfig:"CI_COMMIT_SHORT_SHA" `
CiCommitSha string ` envconfig:"CI_COMMIT_SHA" `
CiCommitTag string ` envconfig:"CI_COMMIT_TAG" `
CiJobId string ` envconfig:"CI_JOB_ID" `
CiJobUrl string ` envconfig:"CI_JOB_URL" `
CiPipelineId string ` envconfig:"CI_PIPELINE_ID" `
CiPipelineUrl string ` envconfig:"CI_PIPELINE_URL" `
2019-05-23 14:01:24 -05:00
}
2019-05-16 10:39:25 -04:00
}
2019-05-23 14:01:24 -05:00
// For additional details refer to https://github.com/kelseyhightower/envconfig
2019-06-24 01:30:18 -08:00
if err := envconfig . Process ( service , & cfg ) ; err != nil {
2019-07-12 23:28:53 -08:00
log . Fatalf ( "main : Parsing Config : %+v" , err )
2019-05-16 10:39:25 -04:00
}
if err := flag . Process ( & cfg ) ; err != nil {
if err != flag . ErrHelp {
2019-07-12 23:28:53 -08:00
log . Fatalf ( "main : Parsing Command Line : %+v" , err )
2019-05-16 10:39:25 -04:00
}
return // We displayed help.
}
// =========================================================================
2019-05-23 14:01:24 -05:00
// Config Validation & Defaults
2019-07-10 00:17:35 -08:00
// AWS access keys are required, if roles are enabled, remove any placeholders.
if cfg . Aws . UseRole {
cfg . Aws . AccessKeyID = ""
cfg . Aws . SecretAccessKey = ""
2019-07-14 01:07:14 -08:00
// 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.
if cfg . Aws . Region == "" {
sess := session . Must ( session . NewSession ( ) )
md := ec2metadata . New ( sess )
var err error
cfg . Aws . Region , err = md . Region ( )
if err != nil {
log . Fatalf ( "main : Load region of ecs metadata : %+v" , err )
}
}
2019-07-10 00:17:35 -08:00
}
2019-07-12 23:28:53 -08:00
// Set the default AWS Secrets Manager prefix used for name to store config files that will be persisted across
// deployments and distributed to each instance of the service running.
if cfg . Aws . SecretsManagerConfigPrefix == "" {
var pts [ ] string
2019-08-14 12:53:40 -08:00
if cfg . Project . Name != "" {
pts = append ( pts , cfg . Project . Name )
2019-07-12 23:28:53 -08:00
}
2019-07-13 12:16:28 -08:00
pts = append ( pts , cfg . Env , cfg . Service . Name )
2019-07-12 23:28:53 -08:00
cfg . Aws . SecretsManagerConfigPrefix = filepath . Join ( pts ... )
}
2019-05-23 14:01:24 -05:00
// If base URL is empty, set the default value from the HTTP Host
2019-07-13 12:16:28 -08:00
if cfg . Service . BaseUrl == "" {
2019-05-23 14:01:24 -05:00
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
}
2019-07-13 12:16:28 -08:00
cfg . Service . BaseUrl = baseUrl
}
// When HTTPS is not specifically enabled, but an HTTP host is set, enable HTTPS.
if ! cfg . Service . EnableHTTPS && cfg . HTTPS . Host != "" {
cfg . Service . EnableHTTPS = true
}
// Determine the primary host by parsing host from the base app URL.
baseSiteUrl , err := url . Parse ( cfg . Service . BaseUrl )
if err != nil {
log . Fatalf ( "main : Parse service base URL : %s : %+v" , cfg . Service . BaseUrl , err )
}
// Drop any ports from the base app URL.
var primaryServiceHost string
if strings . Contains ( baseSiteUrl . Host , ":" ) {
primaryServiceHost , _ , err = net . SplitHostPort ( baseSiteUrl . Host )
if err != nil {
log . Fatalf ( "main : SplitHostPort : %s : %+v" , baseSiteUrl . Host , err )
}
} else {
primaryServiceHost = baseSiteUrl . Host
2019-05-23 14:01:24 -05:00
}
// =========================================================================
2019-07-13 12:16:28 -08:00
// Log Service Info
2019-05-16 10:39:25 -04:00
// Print the build version for our logs. Also expose it under /debug/vars.
expvar . NewString ( "build" ) . Set ( build )
2019-07-13 12:16:28 -08:00
log . Printf ( "main : Started : Service Initializing version %q" , build )
2019-05-16 10:39:25 -04:00
defer log . Println ( "main : Completed" )
2019-05-23 14:01:24 -05:00
// 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 {
2019-07-12 23:28:53 -08:00
log . Fatalf ( "main : Marshalling Config to JSON : %+v" , err )
2019-05-23 14:01:24 -05:00
}
log . Printf ( "main : Config : %v\n" , string ( cfgJSON ) )
2019-05-16 10:39:25 -04:00
}
// =========================================================================
2019-05-17 14:42:50 -04:00
// Init AWS Session
var awsSession * session . Session
2019-05-23 19:40:29 -05:00
if cfg . Aws . UseRole {
2019-05-17 14:42:50 -04:00
// 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.
awsSession = session . Must ( session . NewSession ( ) )
2019-07-14 00:54:51 -08:00
if cfg . Aws . Region != "" {
awsSession . Config . WithRegion ( cfg . Aws . Region )
}
2019-07-11 00:58:45 -08:00
log . Printf ( "main : AWS : Using role.\n" )
2019-07-12 23:28:53 -08:00
} else if cfg . Aws . AccessKeyID != "" {
2019-05-23 19:40:29 -05:00
creds := credentials . NewStaticCredentials ( cfg . Aws . AccessKeyID , cfg . Aws . SecretAccessKey , "" )
awsSession = session . New ( & aws . Config { Region : aws . String ( cfg . Aws . Region ) , Credentials : creds } )
2019-07-11 00:58:45 -08:00
log . Printf ( "main : AWS : Using static credentials\n" )
2019-05-16 10:39:25 -04:00
}
2019-07-12 23:28:53 -08:00
// Wrap the AWS session to enable tracing.
if awsSession != nil {
awsSession = awstrace . WrapSession ( awsSession )
}
2019-08-14 12:53:40 -08:00
// =========================================================================
// Shared Secret Key used for encrypting sessions and links.
// Set the secret key if not provided in the config.
if cfg . Project . SharedSecretKey == "" {
// AWS secrets manager ID for storing the session key. This is optional and only will be used
// if a valid AWS session is provided.
secretID := filepath . Join ( cfg . Aws . SecretsManagerConfigPrefix , "sharedSecretKey" )
// If AWS is enabled, check the Secrets Manager for the session key.
if awsSession != nil {
cfg . Project . SharedSecretKey , err = devops . SecretManagerGetString ( awsSession , secretID )
if err != nil && errors . Cause ( err ) != devops . ErrSecreteNotFound {
log . Fatalf ( "main : Session : %+v" , err )
}
}
// If the session key is still empty, generate a new key.
if cfg . Project . SharedSecretKey == "" {
cfg . Project . SharedSecretKey = string ( securecookie . GenerateRandomKey ( 32 ) )
if awsSession != nil {
err = devops . SecretManagerPutString ( awsSession , secretID , cfg . Project . SharedSecretKey )
if err != nil {
log . Fatalf ( "main : Session : %+v" , err )
}
}
}
}
2019-05-17 14:42:50 -04:00
// =========================================================================
2019-05-23 14:01:24 -05:00
// 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 ( )
2019-07-10 00:17:35 -08:00
if err != nil && ! strings . Contains ( err . Error ( ) , "unknown command" ) {
2019-07-12 23:28:53 -08:00
log . Fatalf ( "main : redis : ConfigSet maxmemory-policy : %+v" , err )
2019-05-23 14:01:24 -05:00
}
} else {
evictPolicy , err := redisClient . ConfigGet ( evictPolicyConfigKey ) . Result ( )
2019-07-10 00:17:35 -08:00
if err != nil && ! strings . Contains ( err . Error ( ) , "unknown command" ) {
2019-07-12 23:28:53 -08:00
log . Fatalf ( "main : redis : ConfigGet maxmemory-policy : %+v" , err )
2019-07-10 00:17:35 -08:00
} else if evictPolicy != nil && len ( evictPolicy ) > 0 && evictPolicy [ 1 ] != "allkeys-lru" {
2019-05-23 14:01:24 -05:00
log . Printf ( "main : redis : ConfigGet maxmemory-policy : recommended to be set to allkeys-lru to avoid OOM" )
}
2019-05-16 10:39:25 -04:00
}
// =========================================================================
2019-05-23 14:01:24 -05:00
// Start Database
var dbUrl url . URL
{
// Query parameters.
2019-05-23 19:40:29 -05:00
var q url . Values = make ( map [ string ] [ ] string )
2019-05-23 14:01:24 -05:00
// 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 ( ) ,
}
}
2019-05-23 19:40:29 -05:00
log . Println ( "main : Started : Initialize Database" )
2019-05-16 10:39:25 -04:00
2019-05-23 14:01:24 -05:00
// 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.
2019-06-24 01:30:18 -08:00
sqltrace . Register ( cfg . DB . Driver , & pq . Driver { } , sqltrace . WithServiceName ( service ) )
2019-05-23 14:01:24 -05:00
masterDb , err := sqlxtrace . Open ( cfg . DB . Driver , dbUrl . String ( ) )
2019-05-16 10:39:25 -04:00
if err != nil {
2019-07-12 23:28:53 -08:00
log . Fatalf ( "main : Register DB : %s : %+v" , cfg . DB . Driver , err )
2019-05-23 14:01:24 -05:00
}
defer masterDb . Close ( )
2019-08-14 12:53:40 -08:00
// =========================================================================
// Notify Email
var notifyEmail notify . Email
if awsSession != nil {
// Send emails with AWS SES. Alternative to use SMTP with notify.NewEmailSmtp.
notifyEmail , err = notify . NewEmailAws ( awsSession , cfg . Project . SharedTemplateDir , cfg . Project . EmailSender )
if err != nil {
log . Fatalf ( "main : Notify Email : %+v" , err )
}
err = notifyEmail . Verify ( )
if err != nil {
switch errors . Cause ( err ) {
case notify . ErrAwsSesIdentityNotVerified :
log . Printf ( "main : Notify Email : %s\n" , err )
case notify . ErrAwsSesSendingDisabled :
log . Printf ( "main : Notify Email : %s\n" , err )
default :
log . Fatalf ( "main : Notify Email Verify : %+v" , err )
}
}
} else {
notifyEmail = notify . NewEmailDisabled ( )
}
2019-05-23 14:01:24 -05:00
// =========================================================================
2019-06-24 17:36:42 -08:00
// Init new Authenticator
var authenticator * auth . Authenticator
if cfg . Auth . UseAwsSecretManager {
2019-07-12 23:28:53 -08:00
secretName := filepath . Join ( cfg . Aws . SecretsManagerConfigPrefix , "authenticator" )
authenticator , err = auth . NewAuthenticatorAws ( awsSession , secretName , time . Now ( ) . UTC ( ) , cfg . Auth . KeyExpiration )
2019-06-24 17:36:42 -08:00
} else {
authenticator , err = auth . NewAuthenticatorFile ( "" , time . Now ( ) . UTC ( ) , cfg . Auth . KeyExpiration )
}
2019-05-23 14:01:24 -05:00
if err != nil {
2019-07-12 23:28:53 -08:00
log . Fatalf ( "main : Constructing authenticator : %+v" , err )
2019-05-16 10:39:25 -04:00
}
2019-07-13 12:16:28 -08:00
// =========================================================================
2019-08-14 12:53:40 -08:00
// Init repositories and AppContext
projectRoute , err := project_route . New ( cfg . Service . BaseUrl , cfg . Project . WebAppBaseUrl )
if err != nil {
log . Fatalf ( "main : project routes : %+v" , cfg . Service . BaseUrl , err )
}
usrRepo := user . NewRepository ( masterDb , projectRoute . UserResetPassword , notifyEmail , cfg . Project . SharedSecretKey )
usrAccRepo := user_account . NewRepository ( masterDb )
accRepo := account . NewRepository ( masterDb )
accPrefRepo := account_preference . NewRepository ( masterDb )
authRepo := user_auth . NewRepository ( masterDb , authenticator , usrRepo , usrAccRepo , accPrefRepo )
signupRepo := signup . NewRepository ( masterDb , usrRepo , usrAccRepo , accRepo )
inviteRepo := invite . NewRepository ( masterDb , usrRepo , usrAccRepo , accRepo , projectRoute . UserInviteAccept , notifyEmail , cfg . Project . SharedSecretKey )
prjRepo := project . NewRepository ( masterDb )
appCtx := & handlers . AppContext {
Log : log ,
Env : cfg . Env ,
MasterDB : masterDb ,
Redis : redisClient ,
UserRepo : usrRepo ,
UserAccountRepo : usrAccRepo ,
AccountRepo : accRepo ,
AccountPrefRepo : accPrefRepo ,
AuthRepo : authRepo ,
SignupRepo : signupRepo ,
InviteRepo : inviteRepo ,
ProjectRepo : prjRepo ,
Authenticator : authenticator ,
2019-07-31 13:47:30 -08:00
}
2019-07-13 12:16:28 -08:00
2019-08-14 12:53:40 -08:00
// =========================================================================
// Load middlewares that need to be configured specific for the service.
2019-07-13 12:16:28 -08:00
// Init redirect middleware to ensure all requests go to the primary domain contained in the base URL.
2019-07-14 17:03:08 -08:00
if primaryServiceHost != "127.0.0.1" && primaryServiceHost != "localhost" {
2019-07-13 12:16:28 -08:00
redirect := mid . DomainNameRedirect ( mid . DomainNameRedirectConfig {
RedirectConfig : mid . RedirectConfig {
Code : http . StatusMovedPermanently ,
Skipper : func ( ctx context . Context , w http . ResponseWriter , r * http . Request , params map [ string ] string ) bool {
if r . URL . Path == "/ping" {
return true
}
return false
} ,
} ,
DomainName : primaryServiceHost ,
HTTPSEnabled : cfg . Service . EnableHTTPS ,
} )
2019-08-14 12:53:40 -08:00
appCtx . PostAppMiddleware = append ( appCtx . PostAppMiddleware , redirect )
2019-07-12 11:41:41 -08:00
}
2019-08-14 12:53:40 -08:00
// Add the translator middleware for localization.
appCtx . PostAppMiddleware = append ( appCtx . PostAppMiddleware , mid . Translator ( webcontext . UniversalTranslator ( ) ) )
2019-05-16 10:39:25 -04:00
// =========================================================================
// Start Tracing Support
2019-05-23 19:40:29 -05:00
th := fmt . Sprintf ( "%s:%d" , cfg . Trace . Host , cfg . Trace . Port )
log . Printf ( "main : Tracing Started : %s" , th )
sr := tracer . NewRateSampler ( cfg . Trace . AnalyticsRate )
tracer . Start ( tracer . WithAgentAddr ( th ) , tracer . WithSampler ( sr ) )
defer tracer . Stop ( )
2019-05-16 10:39:25 -04:00
// =========================================================================
// Start Debug Service. Not concerned with shutting this down when the
// application is being shutdown.
//
// /debug/vars - Added to the default mux by the expvars package.
// /debug/pprof - Added to the default mux by the net/http/pprof package.
2019-07-13 12:16:28 -08:00
if cfg . Service . DebugHost != "" {
2019-05-18 18:06:10 -04:00
go func ( ) {
2019-07-13 12:16:28 -08:00
log . Printf ( "main : Debug Listening %s" , cfg . Service . DebugHost )
log . Printf ( "main : Debug Listener closed : %v" , http . ListenAndServe ( cfg . Service . DebugHost , http . DefaultServeMux ) )
2019-05-18 18:06:10 -04:00
} ( )
}
2019-05-16 10:39:25 -04:00
2019-07-11 00:58:45 -08:00
// =========================================================================
// ECS Task registration for services that don't use an AWS Elastic Load Balancer.
err = devops . EcsServiceTaskInit ( log , awsSession )
if err != nil {
2019-07-12 23:28:53 -08:00
log . Fatalf ( "main : Ecs Service Task init : %+v" , err )
2019-07-11 00:58:45 -08:00
}
2019-05-16 10:39:25 -04:00
// =========================================================================
// Start API Service
2019-06-24 17:36:42 -08:00
// Programmatically set swagger info.
{
docs . SwaggerInfo . Version = build
2019-07-13 12:16:28 -08:00
u , err := url . Parse ( cfg . Service . BaseUrl )
2019-06-24 17:36:42 -08:00
if err != nil {
2019-07-13 12:16:28 -08:00
log . Fatalf ( "main : Parse app base url %s : %+v" , cfg . Service . BaseUrl , err )
2019-06-24 17:36:42 -08:00
}
docs . SwaggerInfo . Host = u . Host
docs . SwaggerInfo . BasePath = "/v1"
}
2019-05-16 10:39:25 -04:00
// Make a channel to listen for an interrupt or terminate signal from the OS.
// Use a buffered channel because the signal package requires it.
shutdown := make ( chan os . Signal , 1 )
signal . Notify ( shutdown , os . Interrupt , syscall . SIGTERM )
// Make a channel to listen for errors coming from the listener. Use a
// buffered channel so the goroutine can exit if we don't collect this error.
serverErrors := make ( chan error , 1 )
2019-07-11 14:15:29 -08:00
// Make an list of HTTP servers for both HTTP and HTTPS requests.
var httpServers [ ] http . Server
// Start the HTTP service listening for requests.
if cfg . HTTP . Host != "" {
api := http . Server {
Addr : cfg . HTTP . Host ,
2019-08-14 12:53:40 -08:00
Handler : handlers . API ( shutdown , appCtx ) ,
2019-07-11 14:15:29 -08:00
ReadTimeout : cfg . HTTP . ReadTimeout ,
WriteTimeout : cfg . HTTP . WriteTimeout ,
MaxHeaderBytes : 1 << 20 ,
}
httpServers = append ( httpServers , api )
go func ( ) {
log . Printf ( "main : API Listening %s" , cfg . HTTP . Host )
serverErrors <- api . ListenAndServe ( )
} ( )
}
2019-07-12 23:28:53 -08:00
// Start the HTTPS service listening for requests with an SSL Cert auto generated with Let's Encrypt.
2019-07-11 14:15:29 -08:00
if cfg . HTTPS . Host != "" {
api := http . Server {
Addr : cfg . HTTPS . Host ,
2019-08-14 12:53:40 -08:00
Handler : handlers . API ( shutdown , appCtx ) ,
2019-07-11 14:15:29 -08:00
ReadTimeout : cfg . HTTPS . ReadTimeout ,
WriteTimeout : cfg . HTTPS . WriteTimeout ,
MaxHeaderBytes : 1 << 20 ,
}
2019-07-12 23:28:53 -08:00
// Generate a unique list of hostnames.
var hosts [ ] string
2019-07-13 12:16:28 -08:00
if primaryServiceHost != "" {
hosts = append ( hosts , primaryServiceHost )
2019-07-12 23:28:53 -08:00
}
2019-07-13 12:16:28 -08:00
for _ , h := range cfg . Service . HostNames {
2019-07-12 23:28:53 -08:00
h = strings . TrimSpace ( h )
2019-07-13 12:16:28 -08:00
if h != "" && h != primaryServiceHost {
2019-07-12 23:28:53 -08:00
hosts = append ( hosts , h )
2019-07-11 14:15:29 -08:00
}
}
2019-07-13 00:15:44 -08:00
// Enable autocert to store certs via Secret Manager.
secretPrefix := filepath . Join ( cfg . Aws . SecretsManagerConfigPrefix , "autocert" )
2019-07-13 03:03:30 -08:00
// Local file cache to reduce requests hitting Secret Manager.
localCache := autocert . DirCache ( os . TempDir ( ) )
cache , err := devops . NewSecretManagerAutocertCache ( log , awsSession , secretPrefix , localCache )
2019-07-13 00:15:44 -08:00
if err != nil {
log . Fatalf ( "main : HTTPS : %+v" , err )
}
2019-07-11 14:15:29 -08:00
m := & autocert . Manager {
Prompt : autocert . AcceptTOS ,
2019-07-12 23:28:53 -08:00
HostPolicy : autocert . HostWhitelist ( hosts ... ) ,
2019-07-13 00:15:44 -08:00
Cache : cache ,
2019-07-11 14:15:29 -08:00
}
api . TLSConfig = & tls . Config { GetCertificate : m . GetCertificate }
2019-07-13 03:03:30 -08:00
api . TLSConfig . NextProtos = append ( api . TLSConfig . NextProtos , acme . ALPNProto )
if ! cfg . HTTPS . DisableHTTP2 {
api . TLSConfig . NextProtos = append ( api . TLSConfig . NextProtos , "h2" )
}
2019-07-11 14:15:29 -08:00
httpServers = append ( httpServers , api )
go func ( ) {
2019-07-13 03:03:30 -08:00
log . Printf ( "main : API Listening %s with SSL cert for hosts %s" , cfg . HTTPS . Host , strings . Join ( hosts , ", " ) )
2019-07-11 14:15:29 -08:00
serverErrors <- api . ListenAndServeTLS ( "" , "" )
} ( )
}
2019-05-16 10:39:25 -04:00
// =========================================================================
// Shutdown
// Blocking main and waiting for shutdown.
select {
case err := <- serverErrors :
2019-07-12 23:28:53 -08:00
log . Fatalf ( "main : Error starting server: %+v" , err )
2019-05-16 10:39:25 -04:00
case sig := <- shutdown :
log . Printf ( "main : %v : Start shutdown.." , sig )
2019-07-11 00:58:45 -08:00
// Ensure the public IP address for the task is removed from Route53.
err = devops . EcsServiceTaskTaskShutdown ( log , awsSession )
if err != nil {
2019-07-12 23:28:53 -08:00
log . Fatalf ( "main : Ecs Service Task shutdown : %+v" , err )
2019-07-11 00:58:45 -08:00
}
2019-05-16 10:39:25 -04:00
// Create context for Shutdown call.
2019-07-13 12:16:28 -08:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , cfg . Service . ShutdownTimeout )
2019-05-16 10:39:25 -04:00
defer cancel ( )
2019-07-11 14:15:29 -08:00
// Handle closing connections for both possible HTTP servers.
for _ , api := range httpServers {
// Asking listener to shutdown and load shed.
err := api . Shutdown ( ctx )
if err != nil {
2019-07-13 12:16:28 -08:00
log . Printf ( "main : Graceful shutdown did not complete in %v : %v" , cfg . Service . ShutdownTimeout , err )
2019-07-11 14:15:29 -08:00
err = api . Close ( )
}
2019-05-16 10:39:25 -04:00
}
// Log the status of this shutdown.
switch {
case sig == syscall . SIGSTOP :
log . Fatal ( "main : Integrity issue caused shutdown" )
case err != nil :
2019-07-12 23:28:53 -08:00
log . Fatalf ( "main : Could not stop server gracefully : %+v" , err )
2019-05-16 10:39:25 -04:00
}
}
}