diff --git a/lib/config/aws.go b/lib/config/aws.go new file mode 100644 index 0000000..4d6a62d --- /dev/null +++ b/lib/config/aws.go @@ -0,0 +1,33 @@ +package config + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" +) + +// AWS configures credentials for access to Amazon Web Services. +// It is intended to be used in composition rather than a key. +type AWS struct { + AccessKeyID string `json:"access_key_id"` + AccessKeySecret string `json:"access_key_secret"` + + Region string `json:"region,omitempty"` +} + +// Credentials returns a configured set of AWS credentials. +func (a AWS) Credentials() *credentials.Credentials { + if a.AccessKeyID != "" && a.AccessKeySecret != "" { + return credentials.NewStaticCredentials(a.AccessKeyID, a.AccessKeySecret, "") + } + + return nil +} + +// Session returns an AWS Session configured with region and credentials. +func (a AWS) Session() (*session.Session, error) { + return session.NewSession(&aws.Config{ + Region: aws.String(a.Region), + Credentials: a.Credentials(), + }) +} diff --git a/lib/config/common.go b/lib/config/common.go deleted file mode 100644 index 80d2022..0000000 --- a/lib/config/common.go +++ /dev/null @@ -1,276 +0,0 @@ -package config - -import ( - "context" - "errors" - "net" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/cuvva/cuvva-public-go/lib/db/mongodb" - "github.com/go-redis/redis" - "github.com/sirupsen/logrus" - "go.mongodb.org/mongo-driver/mongo/options" - "go.mongodb.org/mongo-driver/mongo/readconcern" - "go.mongodb.org/mongo-driver/mongo/writeconcern" - "go.mongodb.org/mongo-driver/x/mongo/driver/connstring" - "golang.org/x/sync/errgroup" -) - -// Redis configures a connection to a Redis database. -type Redis struct { - URI string `json:"uri"` - DialTimeout time.Duration `json:"dial_timeout"` - ReadTimeout time.Duration `json:"read_timeout"` - WriteTimeout time.Duration `json:"write_timeout"` -} - -// Options returns a configured redis.Options structure. -func (r Redis) Options() (*redis.Options, error) { - opts, err := redis.ParseURL(r.URI) - if err != nil { - return nil, err - } - - opts.DialTimeout = r.DialTimeout - opts.ReadTimeout = r.ReadTimeout - opts.WriteTimeout = r.WriteTimeout - - return opts, nil -} - -// Connect returns a connected redis.Client instance. -func (r Redis) Connect() (*redis.Client, error) { - opts, err := r.Options() - if err != nil { - return nil, err - } - - client := redis.NewClient(opts) - - if err := client.Ping().Err(); err != nil { - return client, err - } - - return client, nil -} - -// MongoDB configures a connection to a Mongo database. -type MongoDB struct { - URI string `json:"uri"` - ConnectTimeout time.Duration `json:"connect_timeout"` - MaxConnIdleTime *time.Duration `json:"max_conn_idle_time"` - MaxConnecting *uint64 `json:"max_connecting"` - MaxPoolSize *uint64 `json:"max_pool_size"` - MinPoolSize *uint64 `json:"min_pool_size"` -} - -// Options returns the MongoDB client options and database name. -func (m MongoDB) Options() (opts *options.ClientOptions, dbName string, err error) { - opts = options.Client().ApplyURI(m.URI) - opts.MaxConnIdleTime = m.MaxConnIdleTime - opts.MaxConnecting = m.MaxConnecting - opts.MaxPoolSize = m.MaxPoolSize - opts.MinPoolSize = m.MinPoolSize - - err = opts.Validate() - if err != nil { - return - } - - // all Go services use majority reads/writes, and this is unlikely to change - // if it does change, switch to accepting as an argument - opts.SetReadConcern(readconcern.Majority()) - opts.SetWriteConcern(writeconcern.New(writeconcern.WMajority(), writeconcern.J(true))) - - cs, err := connstring.Parse(m.URI) - if err != nil { - return - } - - dbName = cs.Database - if dbName == "" { - err = errors.New("missing mongo database name") - } - - return -} - -// Connect returns a connected mongo.Database instance. -func (m MongoDB) Connect() (*mongodb.Database, error) { - opts, dbName, err := m.Options() - if err != nil { - return nil, err - } - - if m.ConnectTimeout == 0 { - m.ConnectTimeout = 10 * time.Second - } - - // this package can only be used for service config - // so can only happen at init-time - no need to accept context input - ctx, cancel := context.WithTimeout(context.Background(), m.ConnectTimeout) - defer cancel() - - return mongodb.Connect(ctx, opts, dbName) -} - -// JWT configures public (and optionally private) keys and issuer for -// JSON Web Tokens. It is intended to be used in composition rather than a key. -type JWT struct { - Issuer string `json:"issuer"` - Public string `json:"public"` - Private string `json:"private,omitempty"` -} - -// AWS configures credentials for access to Amazon Web Services. -// It is intended to be used in composition rather than a key. -type AWS struct { - AccessKeyID string `json:"access_key_id"` - AccessKeySecret string `json:"access_key_secret"` - - Region string `json:"region,omitempty"` -} - -// Credentials returns a configured set of AWS credentials. -func (a AWS) Credentials() *credentials.Credentials { - if a.AccessKeyID != "" && a.AccessKeySecret != "" { - return credentials.NewStaticCredentials(a.AccessKeyID, a.AccessKeySecret, "") - } - - return nil -} - -// Session returns an AWS Session configured with region and credentials. -func (a AWS) Session() (*session.Session, error) { - return session.NewSession(&aws.Config{ - Region: aws.String(a.Region), - Credentials: a.Credentials(), - }) -} - -// DefaultGraceful is the graceful shutdown timeout applied when no -// configuration value is given. -const DefaultGraceful = 5 - -// Server configures the binding and security of an HTTP server. -type Server struct { - Addr string `json:"addr"` - - // Graceful enables graceful shutdown and is the time in seconds to wait - // for all outstanding requests to terminate before forceably killing the - // server. When no value is given, DefaultGraceful is used. Graceful - // shutdown is disabled when less than zero. - Graceful int `json:"graceful"` -} - -// ListenAndServe configures a HTTP server and begins listening for clients. -func (cfg *Server) ListenAndServe(srv *http.Server) error { - // only set listen address if none is already configured - if srv.Addr == "" { - srv.Addr = cfg.Addr - } - - if cfg.Graceful == 0 { - cfg.Graceful = DefaultGraceful - } - - stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) - - errs := make(chan error, 1) - - go func() { - err := srv.ListenAndServe() - if err != http.ErrServerClosed { - errs <- err - } - }() - - select { - case err := <-errs: - return err - - case <-stop: - if cfg.Graceful > 0 { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.Graceful)*time.Second) - defer cancel() - - return srv.Shutdown(ctx) - } - - return nil - } -} - -func (cfg *Server) Listen() (net.Listener, error) { - l, err := net.Listen("tcp", cfg.Addr) - if err != nil { - return nil, err - } - - return l, nil -} - -// Serve the HTTP requests on the specified listener, and gracefully close when the context is cancelled. -func (cfg *Server) Serve(ctx context.Context, l net.Listener, srv *http.Server) (err error) { - eg, egCtx := errgroup.WithContext(ctx) - - eg.Go(func() error { - err := srv.Serve(l) - if err == http.ErrServerClosed { - return nil - } - - return err - }) - - eg.Go(func() error { - select { - case <-ctx.Done(): - logrus.Println("shutting down gracefully") - ctx, cancel := context.WithTimeout(context.Background(), cfg.GracefulTimeout()) - defer cancel() - return srv.Shutdown(ctx) - case <-egCtx.Done(): - return nil - } - }) - - return eg.Wait() -} - -func (cfg *Server) GracefulTimeout() time.Duration { - if cfg.Graceful == 0 { - cfg.Graceful = DefaultGraceful - } - - return time.Duration(cfg.Graceful) * time.Second -} - -func ContextWithCancelOnSignal(ctx context.Context) context.Context { - ctx, cancel := context.WithCancel(ctx) - stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) - - go func() { - defer cancel() - select { - case <-stop: - case <-ctx.Done(): - } - }() - - return ctx -} - -// UnderwriterOpts represents the underwriters info/models options. -type UnderwriterOpts struct { - IncludeUnreleased bool `json:"include_unreleased"` -} diff --git a/lib/config/context.go b/lib/config/context.go new file mode 100644 index 0000000..4411824 --- /dev/null +++ b/lib/config/context.go @@ -0,0 +1,24 @@ +package config + +import ( + "context" + "os" + "os/signal" + "syscall" +) + +func ContextWithCancelOnSignal(ctx context.Context) context.Context { + ctx, cancel := context.WithCancel(ctx) + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + go func() { + defer cancel() + select { + case <-stop: + case <-ctx.Done(): + } + }() + + return ctx +} diff --git a/lib/config/jwt.go b/lib/config/jwt.go new file mode 100644 index 0000000..8226912 --- /dev/null +++ b/lib/config/jwt.go @@ -0,0 +1,9 @@ +package config + +// JWT configures public (and optionally private) keys and issuer for +// JSON Web Tokens. It is intended to be used in composition rather than a key. +type JWT struct { + Issuer string `json:"issuer"` + Public string `json:"public"` + Private string `json:"private,omitempty"` +} diff --git a/lib/config/mongo.go b/lib/config/mongo.go new file mode 100644 index 0000000..df8a4ba --- /dev/null +++ b/lib/config/mongo.go @@ -0,0 +1,73 @@ +package config + +import ( + "context" + "errors" + "time" + + "github.com/cuvva/cuvva-public-go/lib/db/mongodb" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readconcern" + "go.mongodb.org/mongo-driver/mongo/writeconcern" + "go.mongodb.org/mongo-driver/x/mongo/driver/connstring" +) + +// MongoDB configures a connection to a Mongo database. +type MongoDB struct { + URI string `json:"uri"` + ConnectTimeout time.Duration `json:"connect_timeout"` + MaxConnIdleTime *time.Duration `json:"max_conn_idle_time"` + MaxConnecting *uint64 `json:"max_connecting"` + MaxPoolSize *uint64 `json:"max_pool_size"` + MinPoolSize *uint64 `json:"min_pool_size"` +} + +// Options returns the MongoDB client options and database name. +func (m MongoDB) Options() (opts *options.ClientOptions, dbName string, err error) { + opts = options.Client().ApplyURI(m.URI) + opts.MaxConnIdleTime = m.MaxConnIdleTime + opts.MaxConnecting = m.MaxConnecting + opts.MaxPoolSize = m.MaxPoolSize + opts.MinPoolSize = m.MinPoolSize + + err = opts.Validate() + if err != nil { + return + } + + // all Go services use majority reads/writes, and this is unlikely to change + // if it does change, switch to accepting as an argument + opts.SetReadConcern(readconcern.Majority()) + opts.SetWriteConcern(writeconcern.New(writeconcern.WMajority(), writeconcern.J(true))) + + cs, err := connstring.Parse(m.URI) + if err != nil { + return + } + + dbName = cs.Database + if dbName == "" { + err = errors.New("missing mongo database name") + } + + return +} + +// Connect returns a connected mongo.Database instance. +func (m MongoDB) Connect() (*mongodb.Database, error) { + opts, dbName, err := m.Options() + if err != nil { + return nil, err + } + + if m.ConnectTimeout == 0 { + m.ConnectTimeout = 10 * time.Second + } + + // this package can only be used for service config + // so can only happen at init-time - no need to accept context input + ctx, cancel := context.WithTimeout(context.Background(), m.ConnectTimeout) + defer cancel() + + return mongodb.Connect(ctx, opts, dbName) +} diff --git a/lib/config/common_test.go b/lib/config/mongo_test.go similarity index 68% rename from lib/config/common_test.go rename to lib/config/mongo_test.go index 1f27c26..3af8079 100644 --- a/lib/config/common_test.go +++ b/lib/config/mongo_test.go @@ -3,30 +3,11 @@ package config import ( "testing" - "github.com/go-redis/redis" "github.com/stretchr/testify/assert" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/writeconcern" ) -func TestRedisOptions(t *testing.T) { - expected := &redis.Options{ - Network: "tcp", - Addr: "localhost:6379", - Password: "password", - DB: 1, - } - - r := Redis{ - URI: "redis://:password@localhost/1", - } - - opts, err := r.Options() - - assert.Nil(t, err) - assert.Equal(t, expected, opts) -} - func TestMongoDBOptions(t *testing.T) { m := &MongoDB{ URI: "mongodb://foo:bar@127.0.0.1/demo?authSource=admin", diff --git a/lib/config/redis.go b/lib/config/redis.go new file mode 100644 index 0000000..1a797e3 --- /dev/null +++ b/lib/config/redis.go @@ -0,0 +1,45 @@ +package config + +import ( + "time" + + "github.com/go-redis/redis" +) + +// Redis configures a connection to a Redis database. +type Redis struct { + URI string `json:"uri"` + DialTimeout time.Duration `json:"dial_timeout"` + ReadTimeout time.Duration `json:"read_timeout"` + WriteTimeout time.Duration `json:"write_timeout"` +} + +// Options returns a configured redis.Options structure. +func (r Redis) Options() (*redis.Options, error) { + opts, err := redis.ParseURL(r.URI) + if err != nil { + return nil, err + } + + opts.DialTimeout = r.DialTimeout + opts.ReadTimeout = r.ReadTimeout + opts.WriteTimeout = r.WriteTimeout + + return opts, nil +} + +// Connect returns a connected redis.Client instance. +func (r Redis) Connect() (*redis.Client, error) { + opts, err := r.Options() + if err != nil { + return nil, err + } + + client := redis.NewClient(opts) + + if err := client.Ping().Err(); err != nil { + return client, err + } + + return client, nil +} diff --git a/lib/config/redis_test.go b/lib/config/redis_test.go new file mode 100644 index 0000000..753b076 --- /dev/null +++ b/lib/config/redis_test.go @@ -0,0 +1,26 @@ +package config + +import ( + "testing" + + "github.com/go-redis/redis" + "github.com/stretchr/testify/assert" +) + +func TestRedisOptions(t *testing.T) { + expected := &redis.Options{ + Network: "tcp", + Addr: "localhost:6379", + Password: "password", + DB: 1, + } + + r := Redis{ + URI: "redis://:password@localhost/1", + } + + opts, err := r.Options() + + assert.Nil(t, err) + assert.Equal(t, expected, opts) +} diff --git a/lib/config/server.go b/lib/config/server.go new file mode 100644 index 0000000..7c9a907 --- /dev/null +++ b/lib/config/server.go @@ -0,0 +1,113 @@ +package config + +import ( + "context" + "net" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" +) + +// DefaultGraceful is the graceful shutdown timeout applied when no +// configuration value is given. +const DefaultGraceful = 5 + +// Server configures the binding and security of an HTTP server. +type Server struct { + Addr string `json:"addr"` + + // Graceful enables graceful shutdown and is the time in seconds to wait + // for all outstanding requests to terminate before forceably killing the + // server. When no value is given, DefaultGraceful is used. Graceful + // shutdown is disabled when less than zero. + Graceful int `json:"graceful"` +} + +// ListenAndServe configures a HTTP server and begins listening for clients. +func (cfg *Server) ListenAndServe(srv *http.Server) error { + // only set listen address if none is already configured + if srv.Addr == "" { + srv.Addr = cfg.Addr + } + + if cfg.Graceful == 0 { + cfg.Graceful = DefaultGraceful + } + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + errs := make(chan error, 1) + + go func() { + err := srv.ListenAndServe() + if err != http.ErrServerClosed { + errs <- err + } + }() + + select { + case err := <-errs: + return err + + case <-stop: + if cfg.Graceful > 0 { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.Graceful)*time.Second) + defer cancel() + + return srv.Shutdown(ctx) + } + + return nil + } +} + +func (cfg *Server) Listen() (net.Listener, error) { + l, err := net.Listen("tcp", cfg.Addr) + if err != nil { + return nil, err + } + + return l, nil +} + +// Serve the HTTP requests on the specified listener, and gracefully close when the context is cancelled. +func (cfg *Server) Serve(ctx context.Context, l net.Listener, srv *http.Server) (err error) { + eg, egCtx := errgroup.WithContext(ctx) + + eg.Go(func() error { + err := srv.Serve(l) + if err == http.ErrServerClosed { + return nil + } + + return err + }) + + eg.Go(func() error { + select { + case <-ctx.Done(): + logrus.Println("shutting down gracefully") + ctx, cancel := context.WithTimeout(context.Background(), cfg.GracefulTimeout()) + defer cancel() + return srv.Shutdown(ctx) + case <-egCtx.Done(): + return nil + } + }) + + return eg.Wait() +} + +func (cfg *Server) GracefulTimeout() time.Duration { + if cfg.Graceful == 0 { + cfg.Graceful = DefaultGraceful + } + + return time.Duration(cfg.Graceful) * time.Second +} diff --git a/lib/config/underwriter.go b/lib/config/underwriter.go new file mode 100644 index 0000000..2ad0b18 --- /dev/null +++ b/lib/config/underwriter.go @@ -0,0 +1,6 @@ +package config + +// UnderwriterOpts represents the underwriters info/models options. +type UnderwriterOpts struct { + IncludeUnreleased bool `json:"include_unreleased"` +}