3
0

Cleanup internal, vendors, cleanup cmd/*

Introduces /pkg for non-intenral packages
This commit is contained in:
Denis Arh 2019-05-24 12:43:29 +02:00
parent b66ed81136
commit 5a9bce44e8
90 changed files with 4168 additions and 1525 deletions

View File

@ -1,24 +1,15 @@
MESSAGING_HTTP_ADDR=:3000
MESSAGING_HTTP_PRETTY_JSON=1
MESSAGING_HTTP_ERROR_TRACING=1
MESSAGING_DB_DSN=crust:crust@tcp(localhost:3306)/crust?collation=utf8mb4_general_ci
MESSAGING_DB_DSN=corteza:corteza@tcp(localhost:3306)/corteza?collation=utf8mb4_general_ci
MESSAGING_DB_PROFILER=stdout
COMPOSE_HTTP_ADDR=:3001
COMPOSE_HTTP_PRETTY_JSON=1
COMPOSE_HTTP_ERROR_TRACING=1
COMPOSE_DB_DSN=crust:crust@tcp(localhost:3306)/crust?collation=utf8mb4_general_ci
COMPOSE_DB_DSN=corteza:corteza@tcp(localhost:3306)/corteza?collation=utf8mb4_general_ci
COMPOSE_DB_PROFILER=stdout
SYSTEM_HTTP_ADDR=:3002
SYSTEM_HTTP_PRETTY_JSON=1
SYSTEM_HTTP_ERROR_TRACING=1
SYSTEM_DB_DSN=crust:crust@tcp(localhost:3306)/crust?collation=utf8mb4_general_ci
SYSTEM_DB_DSN=corteza:corteza@tcp(localhost:3306)/corteza?collation=utf8mb4_general_ci
SYSTEM_DB_PROFILER=stdout
METRICS=1
METRICS_PASSWORD=metrics
AUTH_JWT_SECRET=jwt-secret
AUTH_JWT_DEBUG=1
@ -26,7 +17,4 @@ AUTH_JWT_DEBUG=1
SMTP_HOST=localhost:1025
SMTP_USER=
SMTP_PASS=
SMTP_FROM="Crust" <info@example.tld>
SUBSCRIPTION_KEY=E7ox7cDMmBzsFS15Ub43KKdbBg6gqOYiUhK3nRN0BlpNzt88mHLycahhVfrJCccc
SUBSCRIPTION_DOMAIN=local.crust.tech
SMTP_FROM="Corteza" <info@example.tld>

View File

@ -7,7 +7,7 @@ assignees: ''
---
Thanks for helping us to improve Crust! We welcome all bug reports. Once we've verified and fixed your issue, we'll ping you in the comments to see if you can verify the fix.
Thanks for helping us to improve Corteza! We welcome all bug reports. Once we've verified and fixed your issue, we'll ping you in the comments to see if you can verify the fix.
We'll give you details on what version can be used to test the fix. Additionally, if you are interested in testing fixes that you ***didn't*** report, look for the issues with
the `status/to-test` label. You can pick any of these up for verification. ***You can delete this message portion of the bug report.***

View File

@ -7,7 +7,7 @@ assignees: ''
---
Thanks for helping us to improve Crust! We welcome all feature requests. Once we've addressed your feature request, we'll ping you in the comments to see if you can verify the change.
Thanks for helping us to improve Corteza! We welcome all feature requests. Once we've addressed your feature request, we'll ping you in the comments to see if you can verify the change.
We'll give you details on what version can be used to test the change. Additionally, if you are interested in testing other fixes or feature requests that you ***didn't*** report,
look for the issues with the `status/to-test` label. You can pick any of these up for verification. ***You can delete this message portion of the bug report.***

View File

@ -1 +1 @@
crusttech/crust
cortezaproject/corteza

23
Gopkg.lock generated
View File

@ -44,14 +44,6 @@
pruneopts = "UT"
revision = "982855dad3e1cd68828d2106997539f36fcf6601"
[[projects]]
branch = "master"
digest = "1:b2d8bf10f6279453f6771158d7afcbb2e047714f7d2e2204f2ea9d630d90d4f2"
name = "github.com/crusttech/permit"
packages = ["pkg/permit"]
pruneopts = "UT"
revision = "6c0c4bece8da7f75a52930f3c07d01a5406499bb"
[[projects]]
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
name = "github.com/davecgh/go-spew"
@ -305,11 +297,12 @@
revision = "1555304b9b35fdd2b425bccf1a5613677705e7d0"
[[projects]]
digest = "1:75d51eeab0df85a3cea9e1297ccd3183b20a10cb4b48c753d8ec8d113cc14250"
digest = "1:93a746f1060a8acbcf69344862b2ceced80f854170e1caae089b2834c5fbf7f4"
name = "github.com/prometheus/client_golang"
packages = [
"prometheus",
"prometheus/internal",
"prometheus/promhttp",
]
pruneopts = "UT"
revision = "505eaef017263e299324067d40ca2c48f6a2cf50"
@ -373,6 +366,14 @@
revision = "588a75ec4f32903aa5e39a2619ba6a4631e28424"
version = "v1.2.2"
[[projects]]
digest = "1:08d65904057412fc0270fc4812a1c90c594186819243160dc779a402d4b6d0bc"
name = "github.com/spf13/cast"
packages = ["."]
pruneopts = "UT"
revision = "8c9545af88b134710ab1cd196795e7f2388358d7"
version = "v1.3.0"
[[projects]]
digest = "1:645cabccbb4fa8aab25a956cbcbdf6a6845ca736b2c64e197ca7cbb9d210b939"
name = "github.com/spf13/cobra"
@ -568,7 +569,6 @@
"github.com/SentimensRG/ctx",
"github.com/SentimensRG/ctx/sigctx",
"github.com/crusttech/go-oidc",
"github.com/crusttech/permit/pkg/permit",
"github.com/davecgh/go-spew/spew",
"github.com/dgrijalva/jwt-go",
"github.com/disintegration/imaging",
@ -594,8 +594,9 @@
"github.com/markbates/goth/providers/openidConnect",
"github.com/namsral/flag",
"github.com/pkg/errors",
"github.com/prometheus/client_golang/prometheus",
"github.com/prometheus/client_golang/prometheus/promhttp",
"github.com/spf13/afero",
"github.com/spf13/cast",
"github.com/spf13/cobra",
"github.com/titpetric/factory",
"github.com/titpetric/factory/resputil",

View File

@ -1,69 +1,17 @@
package main
import (
"fmt"
"os"
context "github.com/SentimensRG/ctx"
"github.com/SentimensRG/ctx/sigctx"
_ "github.com/joho/godotenv/autoload"
"github.com/namsral/flag"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
compose "github.com/crusttech/crust/compose"
"github.com/crusttech/crust/internal/logger"
"github.com/crusttech/crust/internal/subscription"
system "github.com/crusttech/crust/system"
"github.com/cortezaproject/corteza-server/compose"
"github.com/cortezaproject/corteza-server/pkg/cli"
)
func main() {
// Initialize default logger
logger.Init(zapcore.DebugLevel)
log := logger.Default().Named("compose")
// New signal-bond context that we will use and
// will get terminated (Done()) on SIGINT or SIGTERM
ctx := context.AsContext(sigctx.New())
// Bind default logger to context
ctx = logger.ContextWithValue(ctx, log)
compose.Flags("compose")
system.Flags("system")
subscription.Flags()
flag.Parse()
if err := system.Init(ctx); err != nil {
log.Fatal("failed to initialize system", zap.Error(err))
}
if err := compose.Init(ctx); err != nil {
log.Fatal("failed to initialize compose", zap.Error(err))
}
var command string
if len(os.Args) > 1 {
command = os.Args[1]
}
switch command {
case "help":
flag.PrintDefaults()
case "provision":
if err := compose.Provision(ctx); err != nil {
println("Failed to provision compose: ", err.Error())
os.Exit(1)
}
default:
// Checks subscription, will os.Exit(1) if there is an error
// Disabled for now, system service is the only one that validates subscription
// ctx = subscription.Monitor(ctx)
compose.StartWatchers(ctx)
if err := compose.StartRestAPI(ctx); err != nil {
log.Fatal("failed to start compose REST API", zap.Error(err))
}
c := compose.InitCompose()
if err := c.Command(cli.Context()).Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

@ -1,159 +0,0 @@
package main
import (
"net"
"net/http"
"os"
"path"
context "github.com/SentimensRG/ctx"
"github.com/SentimensRG/ctx/sigctx"
"github.com/go-chi/chi"
_ "github.com/joho/godotenv/autoload"
"github.com/namsral/flag"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
compose "github.com/crusttech/crust/compose"
"github.com/crusttech/crust/internal/auth"
"github.com/crusttech/crust/internal/logger"
messaging "github.com/crusttech/crust/messaging"
system "github.com/crusttech/crust/system"
"github.com/crusttech/crust/internal/config"
"github.com/crusttech/crust/internal/metrics"
"github.com/crusttech/crust/internal/middleware"
"github.com/crusttech/crust/internal/subscription"
)
// Serves index.html in case the requested file isn't found (or some other os.Stat error)
func serveIndex(assetPath string, indexPath string, serve http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
indexPage := path.Join(assetPath, indexPath)
requestedPage := path.Join(assetPath, r.URL.Path)
_, err := os.Stat(requestedPage)
if err != nil {
http.ServeFile(w, r, indexPage)
return
}
serve.ServeHTTP(w, r)
}
}
func main() {
// Initialize default logger
logger.Init(zapcore.DebugLevel)
log := logger.Default().Named("crust")
// New signal-bond context that we will use and
// will get terminated (Done()) on SIGINT or SIGTERM
ctx := context.AsContext(sigctx.New())
// Bind default logger to context
ctx = logger.ContextWithValue(ctx, log)
var flags struct {
http *config.HTTP
monitor *config.Monitor
}
flags.http = new(config.HTTP).Init()
flags.monitor = new(config.Monitor).Init()
compose.Flags("compose")
messaging.Flags("messaging")
system.Flags("system")
authJwtFlags := new(config.JWT).Init()
subscription.Flags()
flag.Parse()
var command string
if len(os.Args) > 1 {
command = os.Args[1]
}
switch command {
case "help":
flag.PrintDefaults()
case "provision":
if err := system.Provision(ctx); err != nil {
println("Failed to provision system: ", err.Error())
os.Exit(1)
}
if err := compose.Provision(ctx); err != nil {
println("Failed to provision compose: ", err.Error())
os.Exit(1)
}
if err := messaging.Provision(ctx); err != nil {
println("Failed to provision messaging: ", err.Error())
os.Exit(1)
}
default:
// Initialize configuration of our services
if err := system.Init(ctx); err != nil {
log.Fatal("failed to initialize system", zap.Error(err))
}
if err := compose.Init(ctx); err != nil {
log.Fatal("failed to initialize compose", zap.Error(err))
}
if err := messaging.Init(ctx); err != nil {
log.Fatal("failed to initialize messaging", zap.Error(err))
}
// Checks subscription, will os.Exit(1) if there is an error
// Disabled for now, system service is the only one that validates subscription
// ctx = subscription.Monitor(ctx)
log.Info("Starting http server on address " + flags.http.Addr)
listener, err := net.Listen("tcp", flags.http.Addr)
if err != nil {
log.Info("Can't listen on addr " + flags.http.Addr)
}
if flags.monitor.Interval > 0 {
go metrics.NewMonitor(flags.monitor.Interval)
}
system.StartWatchers(ctx)
compose.StartWatchers(ctx)
messaging.StartWatchers(ctx)
r := chi.NewRouter()
// logging, cors and such
middleware.Mount(ctx, r, flags.http)
// Use JWT secret for hmac signer for now
auth.DefaultSigner = auth.HmacSigner(authJwtFlags.Secret)
auth.DefaultJwtHandler, err = auth.JWT(authJwtFlags.Secret, authJwtFlags.Expiry)
if err != nil {
log.Fatal("Error creating JWT Auth", zap.Error(err))
}
r.Route("/api", func(r chi.Router) {
r.Route("/compose", func(r chi.Router) {
compose.MountRoutes(ctx, r)
})
r.Route("/messaging", func(r chi.Router) {
messaging.MountRoutes(ctx, r)
})
r.Route("/system", func(r chi.Router) {
system.MountRoutes(ctx, r)
})
middleware.MountSystemRoutes(ctx, r, flags.http)
})
fileserver := http.FileServer(http.Dir("webapp"))
for _, service := range []string{"admin", "auth", "messaging", "compose"} {
r.HandleFunc("/"+service+"*", serveIndex("webapp", "compose/index.html", fileserver))
}
r.HandleFunc("/*", serveIndex("webapp", "index.html", fileserver))
go http.Serve(listener, r)
<-ctx.Done()
}
}

View File

@ -1,68 +1,17 @@
package main
import (
"fmt"
"os"
context "github.com/SentimensRG/ctx"
"github.com/SentimensRG/ctx/sigctx"
_ "github.com/joho/godotenv/autoload"
"github.com/namsral/flag"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/crusttech/crust/internal/logger"
"github.com/crusttech/crust/internal/subscription"
messaging "github.com/crusttech/crust/messaging"
system "github.com/crusttech/crust/system"
"github.com/cortezaproject/corteza-server/messaging"
"github.com/cortezaproject/corteza-server/pkg/cli"
)
func main() {
// Initialize default logger
logger.Init(zapcore.DebugLevel)
log := logger.Default().Named("messaging")
// New signal-bond context that we will use and
// will get terminated (Done()) on SIGINT or SIGTERM
ctx := context.AsContext(sigctx.New())
// Bind default logger to context
ctx = logger.ContextWithValue(ctx, log)
messaging.Flags("messaging")
system.Flags("system")
subscription.Flags()
flag.Parse()
if err := system.Init(ctx); err != nil {
log.Fatal("failed to initialize system", zap.Error(err))
}
if err := messaging.Init(ctx); err != nil {
log.Fatal("failed to initialize messaging", zap.Error(err))
}
var command string
if len(os.Args) > 1 {
command = os.Args[1]
}
switch command {
case "help":
flag.PrintDefaults()
case "provision":
if err := messaging.Provision(ctx); err != nil {
println("Failed to provision messagign: ", err.Error())
os.Exit(1)
}
default:
// Checks subscription, will os.Exit(1) if there is an error
// Disabled for now, system service is the only one that validates subscription
// ctx = subscription.Monitor(ctx)
if err := messaging.StartRestAPI(ctx); err != nil {
log.Fatal("failed to start messaging REST API", zap.Error(err))
}
m := messaging.InitMessaging()
if err := m.Command(cli.Context()).Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

17
cmd/monolith/main.go Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"fmt"
"os"
"github.com/cortezaproject/corteza-server/monolith"
"github.com/cortezaproject/corteza-server/pkg/cli"
)
func main() {
c := monolith.InitMonolith()
if err := c.Command(cli.Context()).Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

@ -1,13 +0,0 @@
package main
import (
_ "github.com/joho/godotenv/autoload"
"github.com/namsral/flag"
)
func flags(prefix string, mountFlags ...func(...string)) {
for _, mount := range mountFlags {
mount(prefix)
}
flag.Parse()
}

View File

@ -1,33 +0,0 @@
package main
import (
context "github.com/SentimensRG/ctx"
"github.com/SentimensRG/ctx/sigctx"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/crusttech/crust/internal/logger"
system "github.com/crusttech/crust/system"
"github.com/crusttech/crust/system/cli"
)
func main() {
// Initialize default logger
logger.Init(zapcore.DebugLevel)
log := logger.Default().Named("system-cli")
// New signal-bond context that we will use and
// will get terminated (Done()) on SIGINT or SIGTERM
ctx := context.AsContext(sigctx.New())
// Bind default logger to context
ctx = logger.ContextWithValue(ctx, log)
flags("system", system.Flags)
if err := system.Init(ctx); err != nil {
log.Fatal("failed to initialize system", zap.Error(err))
}
cli.StartCLI(ctx)
}

View File

@ -1,62 +1,17 @@
package main
import (
"fmt"
"os"
context "github.com/SentimensRG/ctx"
"github.com/SentimensRG/ctx/sigctx"
_ "github.com/joho/godotenv/autoload"
"github.com/namsral/flag"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/crusttech/crust/internal/logger"
"github.com/crusttech/crust/internal/subscription"
system "github.com/crusttech/crust/system"
"github.com/cortezaproject/corteza-server/pkg/cli"
"github.com/cortezaproject/corteza-server/system"
)
func main() {
// Initialize default logger
logger.Init(zapcore.DebugLevel)
log := logger.Default().Named("system")
// New signal-bond context that we will use and
// will get terminated (Done()) on SIGINT or SIGTERM
ctx := context.AsContext(sigctx.New())
// Bind default logger to context
ctx = logger.ContextWithValue(ctx, log)
system.Flags("system")
subscription.Flags()
flag.Parse()
if err := system.Init(ctx); err != nil {
log.Fatal("failed to initialize system", zap.Error(err))
}
var command string
if len(os.Args) > 1 {
command = os.Args[1]
}
switch command {
case "help":
flag.PrintDefaults()
case "provision":
if err := system.Provision(ctx); err != nil {
println("Failed to provision system: ", err.Error())
os.Exit(1)
}
default:
// Checks subscription, will os.Exit(1) if there is an error
ctx = subscription.Monitor(ctx)
if err := system.StartRestAPI(ctx); err != nil {
log.Fatal("failed to start system REST API", zap.Error(err))
}
s := system.InitSystem()
if err := s.Command(cli.Context()).Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

@ -17,7 +17,7 @@ func (e authError) Error() string {
}
func (e authError) String() string {
return "crust.internal.auth." + string(e)
return "internal.auth." + string(e)
}
func (e authError) New() error {

View File

@ -17,16 +17,19 @@ type (
expiry int64
tokenAuth *jwtauth.JWTAuth
}
jwtSettingsGetter interface {
GetGlobalString(name string) (out string, err error)
}
)
var (
DefaultJwtHandler TokenHandler
)
func SetupDefault(secret string, expiry int) {
// Use JWT secret for hmac signer for now
DefaultSigner = HmacSigner(secret)
DefaultJwtHandler, _ = JWT(secret, int64(expiry))
}
func JWT(secret string, expiry int64) (jwt *token, err error) {
if len(secret) == 0 {
return nil, errors.New("JWT secret missing")

View File

@ -1,53 +0,0 @@
package config
import (
"github.com/namsral/flag"
"github.com/pkg/errors"
)
type (
Database struct {
DSN string
Profiler string
}
)
var dbs map[string]*Database
func (c *Database) Validate() error {
if c == nil {
return nil
}
if c.DSN == "" {
return errors.New("No DB DSN is set, can't connect to database")
}
return nil
}
func (*Database) Init(prefix ...string) *Database {
if dbs == nil {
dbs = make(map[string]*Database)
}
name := "default"
if len(prefix) > 0 {
name = prefix[0]
}
if db := dbs[name]; db != nil {
return db
}
p := func(s string) string {
if len(prefix) > 0 {
return prefix[0] + "-" + s
}
return s
}
db := new(Database)
flag.StringVar(&db.DSN, p("db-dsn"), "", "DSN for database connection (e.g. user:pass@tcp(db1:3306)/dbname?collation=utf8mb4_general_ci)")
flag.StringVar(&db.Profiler, p("db-profiler"), "", "Profiler for DB queries (none, stdout)")
dbs[name] = db
return db
}

View File

@ -1,61 +0,0 @@
package config
import (
"github.com/namsral/flag"
"github.com/pkg/errors"
)
type (
HTTP struct {
Addr string
Logging bool
Pretty bool
Tracing bool
Metrics bool
ClientTSLInsecure bool
MetricsUsername, MetricsPassword string
}
)
var http *HTTP
func (c *HTTP) Validate() error {
if c == nil {
return nil
}
if c.Addr == "" {
return errors.New("No HTTP Addr is set, can't listen for HTTP")
}
if c.Metrics && (c.MetricsUsername == "" || c.MetricsPassword == "") {
return errors.New("We can't have unprotected /metrics, please set METRICS_USERNAME/PASSWORD")
}
return nil
}
func (*HTTP) Init(prefix ...string) *HTTP {
if http != nil {
return http
}
p := func(s string) string {
if len(prefix) > 0 {
return prefix[0] + "-" + s
}
return s
}
http = new(HTTP)
flag.StringVar(&http.Addr, p("http-addr"), ":80", "Listen address for HTTP server")
flag.BoolVar(&http.Logging, p("http-log"), true, "Enable/disable HTTP request log")
flag.BoolVar(&http.Pretty, p("http-pretty-json"), false, "Prettify returned JSON output")
flag.BoolVar(&http.Tracing, p("http-error-tracing"), false, "Return error stack frame")
flag.BoolVar(&http.ClientTSLInsecure, p("http-client-tsl-insecure"), false, "Skip insecure TSL verification on outbound HTTP requests (allow invalid/self-signed certificates)")
flag.BoolVar(&http.Metrics, "metrics", false, "Provide metrics export for prometheus")
flag.StringVar(&http.MetricsUsername, "metrics-username", "metrics", "Provide metrics export for prometheus")
flag.StringVar(&http.MetricsPassword, "metrics-password", "", "Provide metrics export for prometheus")
return http
}

View File

@ -1,28 +0,0 @@
package config
import (
"github.com/namsral/flag"
)
type (
HTTPClient struct {
BaseURL string
Timeout int
}
)
var httpClient *HTTPClient
func (c *HTTPClient) Validate() error {
return nil
}
func (*HTTPClient) Init(prefix ...string) *HTTPClient {
if httpClient != nil {
return httpClient
}
httpClient = new(HTTPClient)
flag.StringVar(&httpClient.BaseURL, "http-client-base-url", "", "HTTP Client Base URL")
flag.IntVar(&httpClient.Timeout, "http-client-timeout", 5, "HTTP Client request timeout (seconds)")
return httpClient
}

View File

@ -1,36 +0,0 @@
package config
import (
"github.com/namsral/flag"
"github.com/pkg/errors"
)
type (
JWT struct {
Secret string
Expiry int64
}
)
var jwt *JWT
func (c *JWT) Validate() error {
if c == nil {
return nil
}
if c.Secret == "" {
return errors.New("JWT Secret not set for AUTH")
}
return nil
}
func (*JWT) Init(prefix ...string) *JWT {
if jwt != nil {
return jwt
}
jwt = new(JWT)
flag.StringVar(&jwt.Secret, "auth-jwt-secret", "", "JWT Secret")
flag.Int64Var(&jwt.Expiry, "auth-jwt-expiry", 60*24*30, "JWT Expiration in minutes")
return jwt
}

View File

@ -1,57 +0,0 @@
package config
// @todo need to decide on settings format & structure...
// type (
// // MessagingSettings holds configuration settings for mesaging service
// MessagingSettings struct {
// Messages struct {
// Body struct {
// // How long can a message be
// MaxLength uint `json:"maxLength,omitempty"`
//
// // On render, convert textual [:)] emoji to graph. empji
// EmojiConvertToPicture bool `json:"emojiConvertToPicture,omitempty"`
// } `json:"body,omitempty"`
//
// Avatars struct {
// // Display image that users use for the avatar
// // if false, it will use initials only
// DisplaySelectedImage bool `json:"displaySelectedImage,omitempty"`
//
// // Enable avatar
// Enabled bool `json:"enabled,omitempty"`
// } `json:"avatars,omitempty"`
// } `json:"messages,omitempty"`
//
// Channels struct {
// Name struct {
// // How long can a name be
// MaxLength uint `json:"maxLength,omitempty"`
//
// // Can we have spaces in channel's name
// AllowSpaces bool `json:"allowSpaces,omitempty"`
// } `json:"name,omitempty"`
//
// Topic struct {
// // How long can a name be
// MaxLength uint `json:"maxLength,omitempty"`
//
// // Can we have spaces in channel's name
// Enabled bool `json:"enabled,omitempty"`
// } `json:"topic,omitempty"`
// } `json:"channels,omitempty"`
// }
// )
//
// func DefaultMessagingSettings() (s MessagingSettings) {
// s.Messages.Body.MaxLength = 10000
// s.Messages.Body.EmojiConvertToPicture = true
// s.Messages.Avatars.DisplaySelectedImage = true
// s.Messages.Avatars.Enabled = true
// s.Channels.Name.MaxLength = 40
// s.Channels.Name.AllowSpaces = true
// s.Channels.Topic.MaxLength = 200
// s.Channels.Topic.Enabled = true
//
// return s
// }

View File

@ -1,27 +0,0 @@
package config
import (
"github.com/namsral/flag"
)
type (
Monitor struct {
Interval int
}
)
var monitor *Monitor
func (c *Monitor) Validate() error {
return nil
}
func (*Monitor) Init(prefix ...string) *Monitor {
if monitor != nil {
return monitor
}
monitor = new(Monitor)
flag.IntVar(&monitor.Interval, "monitor-interval", 300, "Monitor interval (seconds, 0 = disable)")
return monitor
}

View File

@ -1,55 +0,0 @@
package config
import (
"time"
"github.com/namsral/flag"
"github.com/pkg/errors"
"github.com/crusttech/crust/internal/logger"
)
type (
PubSub struct {
Mode string
RedisAddr string
PollingInterval time.Duration
Timeout time.Duration
PingTimeout time.Duration
PingPeriod time.Duration
}
)
var pubsub *PubSub
func (c *PubSub) Validate() error {
switch c.Mode {
case "redis":
if c.Mode == "redis" && c.RedisAddr == "" {
logger.Default().Info("[pubsub] No Redis Address defined for mode=redis, falling back to polling")
c.Mode = "poll"
}
case "poll":
default:
return errors.Errorf("Unknown PubSub.Mode: %s", c.Mode)
}
return nil
}
func (*PubSub) Init(prefix ...string) *PubSub {
if pubsub != nil {
return pubsub
}
pubsub = new(PubSub)
pubsub.Timeout = 15 * time.Second
pubsub.PingTimeout = 60 * time.Second
pubsub.PingPeriod = (pubsub.PingTimeout * 10) / 9
flag.StringVar(&pubsub.Mode, "pubsub", "poll", "Pubsub mode (poll, redis)")
flag.StringVar(&pubsub.RedisAddr, "pubsub-redis", "", "Redis Pub/Sub hostname")
flag.DurationVar(&pubsub.PollingInterval, "pubsub-poll-interval", 3*time.Second, "Pub/Sub polling interval (3s, 12m, 3h...)")
return pubsub
}

View File

@ -1,69 +0,0 @@
package config
import (
"errors"
"strconv"
"strings"
"github.com/namsral/flag"
)
type (
SMTP struct {
Host string
Port int
User string
Pass string
From string
}
)
const defaultSMTPPort = 25
var smtp *SMTP
// Validate
//
// No actual validation here for SMTP, we allow mis/un-configured
func (c *SMTP) Validate() error {
if strings.Contains(c.Host, ":") {
parts := strings.SplitN(c.Host, ":", 2)
c.Port, _ = strconv.Atoi(parts[1])
c.Host = parts[0]
}
return nil
}
// RuntimeValidation is used for run-time configuration validation
func (c *SMTP) RuntimeValidation() error {
if c == nil {
return errors.New("SMTP config missing")
}
if c.Host == "" {
return errors.New("No hostname provided for SMTP")
}
if c.Port == 0 {
return errors.New("No port provided for SMTP")
}
if c.From == "" {
return errors.New("Sender for SMTP is not set")
}
return nil
}
func (*SMTP) Init(prefix ...string) *SMTP {
if smtp != nil {
return smtp
}
smtp = new(SMTP)
flag.StringVar(&smtp.Host, "smtp-host", "", "SMTP hostname (may be host:port)")
flag.IntVar(&smtp.Port, "smtp-port", defaultSMTPPort, "SMTP port number")
flag.StringVar(&smtp.User, "smtp-user", "", "SMTP server username")
flag.StringVar(&smtp.Pass, "smtp-pass", "", "SMTP server password")
flag.StringVar(&smtp.From, "smtp-from", "", "SMTP sender header")
return smtp
}

View File

@ -1,33 +0,0 @@
package config
import (
"github.com/namsral/flag"
)
type (
Subscription struct {
Key string
Domain string
}
)
var subscription *Subscription
func (c *Subscription) Validate() error {
if c == nil {
return nil
}
return nil
}
func (*Subscription) Init(prefix ...string) *Subscription {
if subscription != nil {
return subscription
}
subscription = new(Subscription)
flag.StringVar(&subscription.Key, "subscription-key", "", "Subscription key")
flag.StringVar(&subscription.Domain, "subscription-domain", "", "Domain")
return subscription
}

View File

@ -1,30 +0,0 @@
package config
import (
"time"
)
type (
Websocket struct {
Timeout time.Duration
PingTimeout time.Duration
PingPeriod time.Duration
}
)
var websocket *Websocket
func (c *Websocket) Validate() error {
return nil
}
func (*Websocket) Init(prefix ...string) *Websocket {
if websocket != nil {
return websocket
}
websocket = new(Websocket)
websocket.Timeout = 15 * time.Second
websocket.PingTimeout = 120 * time.Second
websocket.PingPeriod = (websocket.PingTimeout * 9) / 10
return websocket
}

View File

@ -7,8 +7,6 @@ import (
"github.com/pkg/errors"
"github.com/titpetric/factory"
"go.uber.org/zap"
"github.com/crusttech/crust/internal/logger"
)
const (
@ -17,17 +15,19 @@ const (
timeout = 1 * time.Minute
)
func TryToConnect(ctx context.Context, name, dsn, profiler string) (db *factory.DB, err error) {
func TryToConnect(ctx context.Context, log *zap.Logger, name, dsn, profiler string) (db *factory.DB, err error) {
factory.Database.Add(name, dsn)
var (
connErrCh = make(chan error, 1)
log = logger.Default().With(zap.String("name", name), zap.String("dsn", dsn))
)
// This logger is also used inside profiler
log = log.Named("database").With(zap.String("name", name))
defer close(connErrCh)
log.Debug("connecting to the database")
log.Debug("connecting to the database", zap.String("dsn", dsn))
go func() {
var (
@ -48,6 +48,7 @@ func TryToConnect(ctx context.Context, name, dsn, profiler string) (db *factory.
"could not connect",
zap.Error(err),
zap.Int("try", try),
zap.String("dsn", dsn),
zap.Float64("delay", delay.Seconds()),
)
@ -84,11 +85,9 @@ func TryToConnect(ctx context.Context, name, dsn, profiler string) (db *factory.
case "stdout":
db.Profiler = &factory.Database.ProfilerStdout
case "logger":
db.Profiler = ZapProfiler(logger.Default().
Named("database").
// Skip 3 levels in call stack to get to the actual function used
WithOptions(zap.AddCallerSkip(3)).
With(zap.String("connection", name)),
// Skip 3 levels in call stack to get to the actual function used
db.Profiler = ZapProfiler(log.
WithOptions(zap.AddCallerSkip(3)),
)
default:
log.Info("no database query profiler selected")

View File

@ -12,17 +12,20 @@ import (
"time"
"github.com/pkg/errors"
"github.com/crusttech/crust/internal/config"
)
type (
Config struct {
BaseURL string
Timeout int
}
Client struct {
Transport *http.Transport
Client *http.Client
debugLevel DebugLevel
config *config.HTTPClient
config *Config
}
Request http.Request
@ -32,14 +35,10 @@ type (
const (
INFO DebugLevel = "info"
FULL = "full"
FULL DebugLevel = "full"
)
func New(flags *config.HTTPClient) (*Client, error) {
if err := flags.Validate(); err != nil {
return nil, errors.Wrap(err, "creating http client failed")
}
func New(flags *Config) (*Client, error) {
timeout := time.Duration(flags.Timeout) * time.Second
transport := &http.Transport{
@ -49,6 +48,7 @@ func New(flags *config.HTTPClient) (*Client, error) {
TLSHandshakeTimeout: timeout,
}
// @todo migrate to http.DefaultClient & http.DefaultTransport, see internal/http.SetupDefaults
client := &http.Client{
Timeout: timeout,
Transport: transport,

View File

@ -10,5 +10,5 @@ type Fortune struct{}
func (*Fortune) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fortune := "Fortune favors the prepared mind. - Louis Pasteur"
fmt.Fprintf(w, fortune)
_, _ = fmt.Fprint(w, fortune)
}

View File

@ -1,12 +1,10 @@
package http
import (
"net/http/httptest"
"testing"
"net/http/httptest"
"github.com/crusttech/crust/internal/config"
"github.com/crusttech/crust/internal/test"
"github.com/cortezaproject/corteza-server/internal/test"
)
func TestHTTPClient(t *testing.T) {
@ -14,7 +12,7 @@ func TestHTTPClient(t *testing.T) {
server := httptest.NewServer(handler)
defer server.Close()
client, err := New(&config.HTTPClient{
client, err := New(&Config{
Timeout: 10,
})
test.Assert(t, err == nil, "%+v", err)

34
internal/http/default.go Normal file
View File

@ -0,0 +1,34 @@
package http
import (
"crypto/tls"
"net"
"net/http"
"time"
)
// SetupDefaults Reconfigures defaults for HTTP client & transport
func SetupDefaults(timeout time.Duration, tslInsecure bool) {
if tslInsecure {
// This will allow HTTPS requests to insecure hosts (expired, wrong host, self signed, untrusted root...)
// With this enabled, features like OIDC auto-discovery should work on any of examples found on badssl.com.
//
// With SYSTEM_HTTP_CLIENT_TSL_INSECURE=0 (default) next command returns 404 error (expected)
// > ./system external-auth auto-discovery foo-tsl-1 https://expired.badssl.com/
//
// Without SYSTEM_HTTP_CLIENT_TSL_INSECURE=1 next command returns "x509: certificate has expired or is not yet valid"
// > ./system external-auth auto-discovery foo-tsl-1 https://expired.badssl.com/
//
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
http.DefaultTransport.(*http.Transport).DialContext = (&net.Dialer{Timeout: timeout}).DialContext
http.DefaultTransport.(*http.Transport).TLSHandshakeTimeout = timeout
}
if timeout > 0 {
http.DefaultClient.Timeout = timeout
}
http.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}

View File

@ -6,14 +6,17 @@ import (
)
var (
DefaultLevel = zap.NewAtomicLevel()
defaultLogger *zap.Logger
)
func Init(level zapcore.Level) {
DefaultLevel.SetLevel(level)
var (
err error
conf = zap.Config{
Level: zap.NewAtomicLevelAt(level),
Level: DefaultLevel,
Development: false,
Encoding: "json",
EncoderConfig: zap.NewProductionEncoderConfig(),

View File

@ -2,11 +2,11 @@ package mail
import (
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
gomail "gopkg.in/mail.v2"
"github.com/crusttech/crust/internal/config"
)
type (
@ -30,19 +30,40 @@ func init() {
addressCheck = regexp.MustCompile(addressCheckRE)
}
func SetupDialer(config *config.SMTP) {
defaultDialerError = config.RuntimeValidation()
// SetupDialer setups SMTP dialer
//
// Host variable can contain "<host>:<port>" that will override port value
func SetupDialer(host string, port int, user, pass, from string) {
if host == "" {
defaultDialerError = errors.New("No hostname provided for SMTP")
return
}
if strings.Contains(host, ":") {
parts := strings.SplitN(host, ":", 2)
port, _ = strconv.Atoi(parts[1])
host = parts[0]
}
if port == 0 {
defaultDialerError = errors.New("No port provided for SMTP")
return
}
if from == "" {
defaultDialerError = errors.New("Sender for SMTP is not set")
return
}
if defaultDialerError != nil {
return
}
defaultFrom = config.From
defaultFrom = from
defaultDialer = gomail.NewDialer(
config.Host,
config.Port,
config.User,
config.Pass,
host,
port,
user,
pass,
)
}

View File

@ -6,15 +6,14 @@ import (
"github.com/golang/mock/gomock"
"github.com/crusttech/crust/internal/config"
"github.com/crusttech/crust/internal/test"
"github.com/cortezaproject/corteza-server/internal/test"
)
func TestDialerInvalidSetup(t *testing.T) {
defaultDialer = nil
defaultDialerError = nil
SetupDialer(nil)
SetupDialer("", 0, "", "", "")
test.Assert(t, defaultDialerError != nil, "'Missing SMTP configuration' error should be set, got: %v", defaultDialerError)
test.Assert(t, defaultDialer == nil, "defaultDialer should n be set, got: %v", defaultDialer)
}
@ -23,13 +22,7 @@ func TestDialerValidSetup(t *testing.T) {
defaultDialer = nil
defaultDialerError = nil
cfg := &config.SMTP{
Host: "localhost:321",
From: "some@email.tld",
}
cfg.Validate()
SetupDialer(cfg)
SetupDialer("localhost:321", 0, "", "", "some@email.tld")
test.Assert(t, defaultDialerError == nil, "defaultDialerError should be nil, got %v", defaultDialerError)
test.Assert(t, defaultDialer != nil, "defaultDialer should be set, got %v", defaultDialer)

View File

@ -1,33 +0,0 @@
package metrics
import (
"net/http"
"github.com/766b/chi-prometheus"
"github.com/99designs/basicauth-go"
"github.com/go-chi/chi"
"github.com/prometheus/client_golang/prometheus"
"github.com/crusttech/crust/internal/config"
)
// Middleware is the request logger that provides metrics to prometheus
func Middleware(name string) func(http.Handler) http.Handler {
return chiprometheus.NewMiddleware(name)
}
// Handler exports prometheus metrics for /metrics requests
func Handler() http.Handler {
return prometheus.Handler()
}
func MountRoutes(r chi.Router, opts *config.HTTP) {
if opts.Metrics {
r.Group(func(r chi.Router) {
r.Use(basicauth.New("Metrics", map[string][]string{
opts.MetricsUsername: {opts.MetricsPassword},
}))
r.Handle("/metrics", Handler())
})
}
}

View File

@ -1,58 +0,0 @@
package middleware
import (
"context"
"fmt"
"net/http"
"reflect"
"runtime"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/crusttech/crust/internal/config"
"github.com/crusttech/crust/internal/logger"
"github.com/crusttech/crust/internal/metrics"
"github.com/crusttech/crust/internal/version"
)
func Mount(ctx context.Context, r chi.Router, opts *config.HTTP) {
r.Use(handleCORS)
r.Use(middleware.RealIP)
r.Use(middleware.RequestID)
r.Use(ContextLogger(logger.ContextValue(ctx)))
if opts.Logging {
r.Use(LogRequest)
r.Use(LogResponse)
}
if opts.Metrics {
r.Use(metrics.Middleware("crust"))
}
}
func MountSystemRoutes(ctx context.Context, r chi.Router, opts *config.HTTP) {
metrics.MountRoutes(r, opts)
r.Mount("/debug", middleware.Profiler())
r.Get("/version", version.HttpHandler)
r.Get("/routes", func(w http.ResponseWriter, req *http.Request) {
var printRoutes func(chi.Routes, string)
printRoutes = func(r chi.Routes, pfix string) {
routes := r.Routes()
for _, route := range routes {
if route.SubRoutes != nil && len(route.SubRoutes.Routes()) > 0 {
printRoutes(route.SubRoutes, pfix+route.Pattern[:len(route.Pattern)-2])
} else {
for method, fn := range route.Handlers {
fmt.Fprintf(w, "%-8s %-80s -> %s\n", method, pfix+route.Pattern, runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name())
}
}
}
}
printRoutes(r, "")
})
}

View File

@ -3,10 +3,10 @@ package organization
import (
"context"
"github.com/crusttech/crust/system/types"
"github.com/cortezaproject/corteza-server/system/types"
)
func Crust() types.Organisation {
func Corteza() types.Organisation {
return types.Organisation{ID: 1}
}
@ -14,6 +14,6 @@ func GetFromContext(ctx context.Context) types.Organisation {
if orgID, ok := ctx.Value("organizationID").(uint64); ok {
return types.Organisation{ID: orgID}
} else {
return Crust()
return Corteza()
}
}

View File

@ -3,7 +3,7 @@ package payload
import (
"encoding/json"
"github.com/crusttech/crust/internal/payload/incoming"
"github.com/cortezaproject/corteza-server/internal/payload/incoming"
)
func Unmarshal(raw []byte) (*incoming.Payload, error) {

View File

@ -5,10 +5,10 @@ import (
"fmt"
"net/url"
"github.com/crusttech/crust/internal/auth"
"github.com/crusttech/crust/internal/payload/outgoing"
messagingTypes "github.com/crusttech/crust/messaging/types"
systemTypes "github.com/crusttech/crust/system/types"
"github.com/cortezaproject/corteza-server/internal/auth"
"github.com/cortezaproject/corteza-server/internal/payload/outgoing"
messagingTypes "github.com/cortezaproject/corteza-server/messaging/types"
systemTypes "github.com/cortezaproject/corteza-server/system/types"
)
const (

View File

@ -5,7 +5,7 @@ import (
"errors"
"github.com/crusttech/crust/internal/test"
"github.com/cortezaproject/corteza-server/internal/test"
)
// Hello! This file is auto-generated.

View File

@ -3,7 +3,7 @@ package permissions
import (
"testing"
"github.com/crusttech/crust/internal/test"
"github.com/cortezaproject/corteza-server/internal/test"
)
func TestResource(t *testing.T) {

View File

@ -5,7 +5,7 @@ import (
"errors"
"github.com/crusttech/crust/internal/test"
"github.com/cortezaproject/corteza-server/internal/test"
)
// Hello! This file is auto-generated.

View File

@ -3,7 +3,7 @@ package permissions
import (
"testing"
"github.com/crusttech/crust/internal/test"
"github.com/cortezaproject/corteza-server/internal/test"
)
const (

View File

@ -4,7 +4,7 @@ import (
"reflect"
"testing"
"github.com/crusttech/crust/internal/test"
"github.com/cortezaproject/corteza-server/internal/test"
)
// Test role inheritance

View File

@ -8,7 +8,7 @@ import (
"github.com/pkg/errors"
"go.uber.org/zap"
"github.com/crusttech/crust/internal/auth"
"github.com/cortezaproject/corteza-server/internal/auth"
)
type (
@ -34,7 +34,7 @@ const (
// It acts as a caching layer
func Service(ctx context.Context, logger *zap.Logger, repository *repository) (svc *service) {
svc = &service{
f: make(chan bool, 0),
f: make(chan bool),
logger: logger.Named("permissions"),
repository: repository,
@ -117,7 +117,7 @@ func (svc service) Watch(ctx context.Context) {
for {
select {
case <-ctx.Done():
break
return
case <-ticker.C:
svc.Reload(ctx)
case <-svc.f:

View File

@ -5,7 +5,7 @@ import (
"errors"
"github.com/crusttech/crust/internal/test"
"github.com/cortezaproject/corteza-server/internal/test"
)
// Hello! This file is auto-generated.

View File

@ -5,7 +5,7 @@ import (
"github.com/jmoiron/sqlx/types"
"github.com/crusttech/crust/internal/test"
"github.com/cortezaproject/corteza-server/internal/test"
)
func TestKV_Bool(t *testing.T) {

View File

@ -5,7 +5,7 @@ import (
"io"
"testing"
"github.com/crusttech/crust/internal/test"
"github.com/cortezaproject/corteza-server/internal/test"
)
func TestStore(t *testing.T) {

View File

@ -2,14 +2,12 @@ package store
import (
"io"
"mime/multipart"
"net/url"
"github.com/pkg/errors"
"github.com/crusttech/crust/internal/config"
"github.com/crusttech/crust/internal/http"
"github.com/cortezaproject/corteza-server/internal/http"
)
func FromURL(fileURL string) (io.ReadCloser, error) {
@ -19,9 +17,10 @@ func FromURL(fileURL string) (io.ReadCloser, error) {
return nil, errors.New("Only HTTPS is supported for file uploads")
}
client, err := http.New(&config.HTTPClient{
client, err := http.New(&http.Config{
Timeout: 10,
})
if err != nil {
return nil, errors.WithStack(err)
}

View File

@ -1,97 +0,0 @@
package subscription
import (
"context"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
"github.com/crusttech/permit/pkg/permit"
"github.com/crusttech/crust/internal/logger"
)
const (
// How many times we try to get the licence
checkMaxTries = 10
// Number of seconds between each try (x number of tries)
checkTryDelay = 10
// Timeout in seconds
checkTimeout = 30
)
// Check for subscription
func Check(ctx context.Context) (err error) {
var (
done context.CancelFunc
p *permit.Permit
try = 1
in = permit.Permit{
Key: flags.subscription.Key,
Domain: flags.subscription.Domain,
}
)
// Do not collect stats on local domains.
// if p.Domain != "local.crust.tech" {
// @todo collect & pass attributes (no of users....) to be validated by permit.crust.tech subscription server.
in.Attributes = map[string]int{
"messaging.enabled": 1,
// "messaging.max-public-channels": 1,
// "messaging.max-messages": 1,
// "messaging.max-users": 1,
// "messaging.max-private-channels": 1,
"system.enabled": 1,
// "system.max-organisations": 1,
// "system.max-users": 1,
// "system.max-teams": 1,
"compose.enabled": 1,
// "compose.max-modules": 1,
// "compose.max-pages": 1,
// "compose.max-triggers": 1,
// "compose.max-users": 1,
// "compose.max-namespaces": 1,
// "compose.max-charts": 1,
}
// }
for {
ctx, done = context.WithTimeout(ctx, time.Second*checkTimeout)
defer done()
p, err = permit.Check(ctx, in)
if err == nil {
break
}
if err != nil {
logger.Default().Warn(
"unable to check for subscription",
zap.Error(err),
zap.Int("try", try),
zap.Int("max", checkMaxTries),
)
}
if try >= checkMaxTries {
return errors.Wrap(err, "unable to check for subscription")
}
time.Sleep(time.Second * checkTryDelay * checkTryDelay)
try++
}
if !p.IsValid() {
return err
} else if p.Domain != flags.subscription.Domain {
return errors.Errorf("subscription domains do not match (%s <> %s)", p.Domain, flags.subscription.Domain)
}
return nil
}

View File

@ -1,35 +0,0 @@
package subscription
import (
"github.com/crusttech/crust/internal/config"
)
type (
localFlags struct {
subscription *config.Subscription
}
)
var flags *localFlags
// Flags matches signature for main()
func Flags(prefix ...string) {
new(localFlags).Init(prefix...)
}
func (f *localFlags) Validate() error {
if err := f.subscription.Validate(); err != nil {
return err
}
return nil
}
func (f *localFlags) Init(prefix ...string) *localFlags {
if flags != nil {
return flags
}
flags = &localFlags{
new(config.Subscription).Init(prefix...),
}
return flags
}

View File

@ -1,59 +0,0 @@
package subscription
import (
"context"
"os"
"time"
"go.uber.org/zap"
"github.com/crusttech/crust/internal/logger"
)
// Starts subscription checker
func Monitor(ctx context.Context) context.Context {
log := logger.Default()
check := func(ctx context.Context) bool {
log.Debug("validating subscription")
if err := Check(ctx); err != nil {
log.Error("subscription could not be validated", zap.Error(err))
return false
} else {
log.Info("subscription validated")
}
return true
}
if !check(ctx) {
// Initial subscription check failed,
// Just exit.
os.Exit(1)
}
// Initialize new context with cancellation we'll return this context and use it from this point on so that we make
// a clean exist in case subscription checking fails
ctx, cancel := context.WithCancel(ctx)
go func() {
// Validate subscription key every 24h ours (from the last start of the service)
t := time.NewTicker(time.Hour * 24)
defer t.Stop()
for {
select {
case <-t.C:
// Check the subscription again and call cancel on context
if !check(ctx) {
cancel()
os.Exit(1)
}
case <-ctx.Done():
return
}
}
}()
return ctx
}

195
monolith/monolith.go Normal file
View File

@ -0,0 +1,195 @@
package monolith
import (
"context"
"github.com/go-chi/chi"
_ "github.com/joho/godotenv/autoload"
"github.com/spf13/cobra"
"go.uber.org/zap"
"github.com/cortezaproject/corteza-server/compose"
"github.com/cortezaproject/corteza-server/internal/logger"
"github.com/cortezaproject/corteza-server/messaging"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/pkg/cli"
"github.com/cortezaproject/corteza-server/pkg/cli/flags"
"github.com/cortezaproject/corteza-server/system"
)
type (
// Sets up compose messaging & system subservices and runs them as one.
Monolith struct {
log *zap.Logger
// General
logOpt *flags.LogOpt
smtpOpt *flags.SMTPOpt
jwtOpt *flags.JWTOpt
httpClientOpt *flags.HttpClientOpt
compose subservice
messaging subservice
system subservice
}
subservice interface {
AddCommands(cmd *cobra.Command, ctx context.Context)
BindApiServerFlags(cmd *cobra.Command)
StartServices(ctx context.Context) (err error)
ApiServerPreRun(ctx context.Context) error
ApiServerRoutes(r chi.Router)
ProvisionMigrateDatabase(ctx context.Context) error
ProvisionAccessControl(ctx context.Context) error
}
runner func(context.Context) error
runners []runner
)
func (rr runners) run(ctx context.Context) (err error) {
for i := range rr {
err = rr[i](ctx)
if err != nil {
return
}
}
return
}
func init() {
logger.Init(zap.DebugLevel)
}
func InitMonolith() *Monolith {
return &Monolith{
log: logger.Default(),
compose: compose.InitCompose(),
messaging: messaging.InitMessaging(),
system: system.InitSystem(),
}
}
// Command produces cobra.Command
func (m *Monolith) Command(ctx context.Context) (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "corteza-server-monolith",
TraverseChildren: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
cli.InitGeneralServices(m.logOpt, m.smtpOpt, m.jwtOpt, m.httpClientOpt)
return m.StartServices(ctx)
},
}
m.BindGlobalFlags(cmd)
srv := api.NewServer(m.log)
serveApiCmd := srv.Command(ctx, "", m.ApiServerPreRun)
// Bind all flags we need for serving monolith
m.BindApiServerFlags(serveApiCmd)
srv.MountRoutes(m.ApiServerRoutes)
cmd.AddCommand(
serveApiCmd,
cli.SetupProvisionSubcommands(ctx, m),
)
m.AddCommands(cmd, ctx)
return
}
// AddCommands - other commands that this subservices need
//
// We wrap each seubservice's set into a subcommand so that we do not get naming collisions
func (m *Monolith) AddCommands(cmd *cobra.Command, ctx context.Context) {
var (
composeCmd = &cobra.Command{Use: "compose"}
messagingCmd = &cobra.Command{Use: "messaging"}
systemCmd = &cobra.Command{Use: "system"}
)
m.compose.AddCommands(composeCmd, ctx)
if len(composeCmd.Commands()) > 0 {
cmd.AddCommand(composeCmd)
}
m.messaging.AddCommands(messagingCmd, ctx)
if len(messagingCmd.Commands()) > 0 {
cmd.AddCommand(messagingCmd)
}
m.system.AddCommands(systemCmd, ctx)
if len(systemCmd.Commands()) > 0 {
cmd.AddCommand(systemCmd)
}
}
// Binds all global flags
func (m *Monolith) BindGlobalFlags(cmd *cobra.Command) {
m.logOpt = flags.Log(cmd)
m.smtpOpt = flags.SMTP(cmd)
m.jwtOpt = flags.JWT(cmd)
m.httpClientOpt = flags.HttpClient(cmd)
}
// BindApiServerFlags sets & binds all API server flags
func (m *Monolith) BindApiServerFlags(cmd *cobra.Command) {
m.compose.BindApiServerFlags(cmd)
m.messaging.BindApiServerFlags(cmd)
m.system.BindApiServerFlags(cmd)
}
func (m *Monolith) StartServices(ctx context.Context) (err error) {
return (runners{
m.compose.StartServices,
m.messaging.StartServices,
m.system.StartServices,
}).run(ctx)
}
// ApiServerPreRun is executed before serve-api command runs REST API server
//
// Should initialize all that needs to run in the background
func (m Monolith) ApiServerPreRun(ctx context.Context) error {
return (runners{
m.compose.ApiServerPreRun,
m.messaging.ApiServerPreRun,
m.system.ApiServerPreRun,
}).run(ctx)
}
// ApiServerRoutes mounts api server routes
func (m *Monolith) ApiServerRoutes(r chi.Router) {
r.Route("/compose", m.compose.ApiServerRoutes)
r.Route("/messaging", m.messaging.ApiServerRoutes)
r.Route("/system", m.system.ApiServerRoutes)
}
// ProvisionMigrateDatabase migrates database to new version
//
// This is ran by default on serve-api (when not explicitly disabled with --compose-provision-database=false)
// or on demand with "provision migrate-database"
func (m Monolith) ProvisionMigrateDatabase(ctx context.Context) error {
return (runners{
m.compose.ProvisionMigrateDatabase,
m.messaging.ProvisionMigrateDatabase,
m.system.ProvisionMigrateDatabase,
}).run(ctx)
}
// ProvisionAccessControl resets access-control rules for roles admin (2) and everyone (1)
//
// Run with emand with "provision access-control-rules"
func (m Monolith) ProvisionAccessControl(ctx context.Context) error {
return (runners{
m.compose.ProvisionAccessControl,
m.messaging.ProvisionAccessControl,
m.system.ProvisionAccessControl,
}).run(ctx)
}

View File

@ -1,4 +1,4 @@
package middleware
package api
import (
"net/http"

37
pkg/api/debug.go Normal file
View File

@ -0,0 +1,37 @@
package api
import (
"fmt"
"net/http"
"reflect"
"runtime"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
)
func Debug(r chi.Router) {
r.Mount("/debug", middleware.Profiler())
DebugRoutes(r)
}
func DebugRoutes(r chi.Router) {
r.Get("/debug/routes", func(w http.ResponseWriter, req *http.Request) {
var printRoutes func(chi.Routes, string)
printRoutes = func(r chi.Routes, pfix string) {
routes := r.Routes()
for _, route := range routes {
if route.SubRoutes != nil && len(route.SubRoutes.Routes()) > 0 {
printRoutes(route.SubRoutes, pfix+route.Pattern[:len(route.Pattern)-2])
} else {
for method, fn := range route.Handlers {
fmt.Fprintf(w, "%-8s %-80s -> %s\n", method, pfix+route.Pattern, runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name())
}
}
}
}
printRoutes(r, "")
})
}

View File

@ -1,4 +1,4 @@
package middleware
package api
import (
"net/http"
@ -8,14 +8,14 @@ import (
"github.com/go-chi/chi/middleware"
"go.uber.org/zap"
"github.com/crusttech/crust/internal/logger"
"github.com/cortezaproject/corteza-server/internal/logger"
)
// ContextLogger middleware binds logger to request's context.
// contextLogger middleware binds logger to request's context.
//
// This allows us to use logger from context (with requestID)
// inside our (generated) handers and controllers
func ContextLogger(log *zap.Logger) func(next http.Handler) http.Handler {
func contextLogger(log *zap.Logger) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var requestID = middleware.GetReqID(req.Context())
@ -34,7 +34,7 @@ func ContextLogger(log *zap.Logger) func(next http.Handler) http.Handler {
// LogRequest middleware logs request details
//
// It uses logger from context, see ContextLogger()
// It uses logger from context, see contextLogger()
func LogRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var remote = req.RemoteAddr
@ -55,7 +55,7 @@ func LogRequest(next http.Handler) http.Handler {
// LogResponse middleware logs response details
//
// It uses logger from context, see ContextLogger()
// It uses logger from context, see contextLogger()
func LogResponse(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
wrapped := middleware.NewWrapResponseWriter(w, req.ProtoMajor)
@ -68,7 +68,7 @@ func LogResponse(next http.Handler) http.Handler {
zap.String("path", req.URL.Path),
zap.Int("status", wrapped.Status()),
zap.Int("size", wrapped.BytesWritten()),
zap.Float64("duration", time.Now().Sub(t).Seconds()),
zap.Float64("duration", time.Since(t).Seconds()),
)
}()

24
pkg/api/metrics.go Normal file
View File

@ -0,0 +1,24 @@
package api
import (
"net/http"
"github.com/766b/chi-prometheus"
"github.com/99designs/basicauth-go"
"github.com/go-chi/chi"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// Middleware is the request logger that provides metrics to prometheus
func Middleware(name string) func(http.Handler) http.Handler {
return chiprometheus.NewMiddleware(name)
}
func Mount(r chi.Router, username, password string) {
r.Group(func(r chi.Router) {
r.Use(basicauth.New("Metrics", map[string][]string{
username: {password},
}))
r.Handle("/metrics", promhttp.Handler())
})
}

24
pkg/api/middleware.go Normal file
View File

@ -0,0 +1,24 @@
package api
import (
"net/http"
"github.com/go-chi/chi/middleware"
"go.uber.org/zap"
)
func Base() []func(http.Handler) http.Handler {
return []func(http.Handler) http.Handler{
handleCORS,
middleware.RealIP,
middleware.RequestID,
}
}
func Logging(log *zap.Logger) []func(http.Handler) http.Handler {
return []func(http.Handler) http.Handler{
contextLogger(log),
LogRequest,
LogResponse,
}
}

View File

@ -1,4 +1,4 @@
package metrics
package api
import (
"expvar"
@ -7,7 +7,7 @@ import (
"go.uber.org/zap"
"github.com/crusttech/crust/internal/logger"
"github.com/cortezaproject/corteza-server/internal/logger"
)
type Monitor struct {

129
pkg/api/server.go Normal file
View File

@ -0,0 +1,129 @@
package api
import (
"context"
"fmt"
"net"
"net/http"
"github.com/go-chi/chi"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/titpetric/factory/resputil"
"go.uber.org/zap"
"github.com/cortezaproject/corteza-server/internal/auth"
"github.com/cortezaproject/corteza-server/internal/version"
"github.com/cortezaproject/corteza-server/pkg/cli/flags"
)
type (
Server struct {
name string
log *zap.Logger
httpOpt *flags.HTTPOpt
monitorOpt *flags.MonitorOpt
endpoints []func(r chi.Router)
}
)
func NewServer(log *zap.Logger) *Server {
return &Server{
endpoints: make([]func(r chi.Router), 0),
log: log.Named("http"),
}
}
func (s *Server) Command(ctx context.Context, prefix string, preRun func(context.Context) error) (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "serve-api",
Short: "Start HTTP Server with REST API",
// Connect all the wires, prepare services, run watchers, bind endpoints
PreRun: func(cmd *cobra.Command, args []string) {
if s.monitorOpt.Interval > 0 {
go NewMonitor(s.monitorOpt.Interval)
}
preRun(ctx)
},
// Run the server
RunE: func(cmd *cobra.Command, args []string) error {
return s.Serve(ctx)
},
}
s.BindApiServerFlags(cmd, prefix)
return
}
func (s *Server) BindApiServerFlags(cmd *cobra.Command, prefix string) {
s.httpOpt = flags.HTTP(cmd, prefix)
s.monitorOpt = flags.Monitor(cmd, prefix)
}
func (s *Server) MountRoutes(mm ...func(chi.Router)) {
s.endpoints = append(s.endpoints, mm...)
}
func (s Server) Serve(ctx context.Context) error {
s.log.Info("Starting HTTP server with REST API", zap.String("address", s.httpOpt.Addr))
// configure resputil options
resputil.SetConfig(resputil.Options{
Pretty: s.httpOpt.Pretty,
Trace: s.httpOpt.Tracing,
Logger: func(err error) {
// @todo: error logging
},
})
listener, err := net.Listen("tcp", s.httpOpt.Addr)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Can't listen on addr %s", s.httpOpt.Addr))
}
router := chi.NewRouter()
router.Use(Base()...)
if s.httpOpt.Logging {
router.Use(Logging(s.log)...)
}
if s.httpOpt.EnableMetrics {
router.Use(Middleware(s.httpOpt.MetricsServiceLabel))
}
router.Group(func(r chi.Router) {
r.Use(
auth.DefaultJwtHandler.Verifier(),
auth.DefaultJwtHandler.Authenticator(),
)
for _, mountRoutes := range s.endpoints {
mountRoutes(r)
}
})
if s.httpOpt.EnableMetrics {
Mount(router, s.httpOpt.MetricsUsername, s.httpOpt.MetricsPassword)
}
if s.httpOpt.EnableDebugRoute {
Debug(router)
}
if s.httpOpt.EnableVersionRoute {
router.Get("/version", version.HttpHandler)
}
go http.Serve(listener, router)
<-ctx.Done()
return nil
}

16
pkg/cli/context.go Normal file
View File

@ -0,0 +1,16 @@
package cli
import (
"context"
ctxwrap "github.com/SentimensRG/ctx"
"github.com/SentimensRG/ctx/sigctx"
)
// Context is small wrapper that returns sig-term bound context
//
// This can be used as (proper) background context that properly terminates
// all subroutines.
func Context() context.Context {
return ctxwrap.AsContext(sigctx.New())
}

26
pkg/cli/flags/db.go Normal file
View File

@ -0,0 +1,26 @@
package flags
import (
"github.com/spf13/cobra"
)
type (
DBOpt struct {
DSN string
Profiler string
}
)
func DB(cmd *cobra.Command, pfix string) (o *DBOpt) {
o = &DBOpt{}
bindString(cmd, &o.DSN,
pFlag(pfix, "db-dsn"), "corteza:corteza@tcp(db:3306)/corteza?collation=utf8mb4_general_ci",
"DSN for database connection")
bindString(cmd, &o.Profiler,
pFlag(pfix, "db-profiler"), "none",
"Profiler for DB queries (none, stdout, logger)")
return
}

56
pkg/cli/flags/flags.go Normal file
View File

@ -0,0 +1,56 @@
package flags
import (
"os"
"strings"
"time"
"github.com/spf13/cast"
"github.com/spf13/cobra"
)
// Prefixes flag
func pFlag(pfix, name string) string {
if pfix != "" {
name = pfix + "-" + name
}
return name
}
// Converts input (flag-name) into ENVIRONMENTAL_VARIABLE_KEY
func envKey(s string) string {
return strings.ToUpper(strings.ReplaceAll(s, "-", "_"))
}
func bindString(cmd *cobra.Command, v *string, flag, def string, desc string) {
if env, has := os.LookupEnv(envKey(flag)); has {
def = cast.ToString(env)
}
cmd.Flags().StringVar(v, flag, def, desc)
}
func bindBool(cmd *cobra.Command, v *bool, flag string, def bool, desc string) {
if env, has := os.LookupEnv(envKey(flag)); has {
def = cast.ToBool(env)
}
cmd.Flags().BoolVar(v, flag, def, desc)
}
func bindInt(cmd *cobra.Command, v *int, flag string, def int, desc string) {
if env, has := os.LookupEnv(envKey(flag)); has {
def = cast.ToInt(env)
}
cmd.Flags().IntVar(v, flag, def, desc)
}
func bindDuration(cmd *cobra.Command, v *time.Duration, flag string, def time.Duration, desc string) {
if env, has := os.LookupEnv(envKey(flag)); has {
def = cast.ToDuration(env)
}
cmd.Flags().DurationVar(v, flag, def, desc)
}

71
pkg/cli/flags/http.go Normal file
View File

@ -0,0 +1,71 @@
package flags
import (
"github.com/spf13/cobra"
"github.com/cortezaproject/corteza-server/internal/rand"
)
type (
HTTPOpt struct {
Addr string
Logging bool
Pretty bool
Tracing bool
EnableVersionRoute bool
EnableDebugRoute bool
EnableMetrics bool
MetricsServiceLabel string
MetricsUsername string
MetricsPassword string
}
)
func HTTP(cmd *cobra.Command, pfix string) (o *HTTPOpt) {
o = &HTTPOpt{}
bindString(cmd, &o.Addr,
pFlag(pfix, "http-addr"), ":80",
"Listen address for HTTP server")
bindBool(cmd, &o.Logging,
pFlag(pfix, "http-log"), true,
"Enable/disable HTTP request log")
bindBool(cmd, &o.Pretty,
pFlag(pfix, "http-pretty-json"), false,
"Prettify returned JSON output")
bindBool(cmd, &o.Tracing,
pFlag(pfix, "http-error-tracing"), false,
"Return error stack frame")
bindBool(cmd, &o.EnableVersionRoute,
pFlag(pfix, "http-enable-version-route"), true,
"Enable /version route")
bindBool(cmd, &o.EnableDebugRoute,
pFlag(pfix, "http-enable-debug-route"), false,
"Enable /debug route with pprof data")
bindBool(cmd, &o.EnableMetrics,
pFlag(pfix, "http-metrics"), false,
"Enable metrics")
bindString(cmd, &o.MetricsServiceLabel,
pFlag(pfix, "http-metrics-name"), "corteza",
"Provide metrics service label for Prometheus")
bindString(cmd, &o.MetricsUsername,
pFlag(pfix, "http-metrics-username"), "metrics",
"Provide metrics username for Prometheus")
// Setting metrics password to random string to prevent security accidents...
bindString(cmd, &o.MetricsPassword,
pFlag(pfix, "http-metrics-password"), string(rand.Bytes(5)),
"Provide metrics password for Prometheus")
return
}

View File

@ -0,0 +1,28 @@
package flags
import (
"time"
"github.com/spf13/cobra"
)
type (
HttpClientOpt struct {
ClientTSLInsecure bool
HttpClientTimeout time.Duration
}
)
func HttpClient(cmd *cobra.Command) (o *HttpClientOpt) {
o = &HttpClientOpt{}
bindBool(cmd, &o.ClientTSLInsecure,
"http-client-tsl-insecure", false,
"Skip insecure TSL verification on outbound HTTP requests (allow invalid/self-signed certificates")
bindDuration(cmd, &o.HttpClientTimeout,
"http-client-timeout", 30*time.Second,
"Default HTTP client timeout")
return
}

29
pkg/cli/flags/jwt.go Normal file
View File

@ -0,0 +1,29 @@
package flags
import (
"github.com/spf13/cobra"
"github.com/cortezaproject/corteza-server/internal/rand"
)
type (
JWTOpt struct {
Secret string
Expiry int
}
)
func JWT(cmd *cobra.Command) (o *JWTOpt) {
o = &JWTOpt{}
// Setting JWT secret to random string to prevent security accidents...
bindString(cmd, &o.Secret,
"auth-jwt-secret", string(rand.Bytes(32)),
"JWT Secret")
bindInt(cmd, &o.Expiry,
"auth-jwt-expiry", 60*24*30,
"JWT Expiration in minutes")
return
}

26
pkg/cli/flags/log.go Normal file
View File

@ -0,0 +1,26 @@
package flags
import (
"github.com/spf13/cobra"
)
type (
LogOpt struct {
Level string
JSON bool
}
)
func Log(cmd *cobra.Command) (o *LogOpt) {
o = &LogOpt{}
bindString(cmd, &o.Level,
"log-level", "info",
"Log level (debug, info, warn, error, panic, fatal)")
bindBool(cmd, &o.JSON,
"log-json", true,
"Log in JSON format")
return
}

21
pkg/cli/flags/monitor.go Normal file
View File

@ -0,0 +1,21 @@
package flags
import (
"github.com/spf13/cobra"
)
type (
MonitorOpt struct {
Interval int
}
)
func Monitor(cmd *cobra.Command, pfix string) (o *MonitorOpt) {
o = &MonitorOpt{}
bindInt(cmd, &o.Interval,
pFlag(pfix, "monitor-interval"), 300,
"Monitor interval (seconds, 0 = disable)")
return
}

View File

@ -0,0 +1,21 @@
package flags
import (
"github.com/spf13/cobra"
)
type (
ProvisionOpt struct {
Database bool
}
)
func Provision(cmd *cobra.Command, pfix string) (o *ProvisionOpt) {
o = &ProvisionOpt{}
bindBool(cmd, &o.Database,
pFlag(pfix, "provision-database"), true,
"Run database migration scripts")
return
}

58
pkg/cli/flags/pubsub.go Normal file
View File

@ -0,0 +1,58 @@
package flags
import (
"time"
"github.com/spf13/cobra"
)
type (
PubSubOpt struct {
Mode string
// Mode
PollingInterval time.Duration
// Redis
RedisAddr string
RedisTimeout time.Duration
RedisPingTimeout time.Duration
RedisPingPeriod time.Duration
}
)
func PubSub(cmd *cobra.Command, pfix string) (o *PubSubOpt) {
o = &PubSubOpt{}
const (
timeout = 15 * time.Second
pingTimeout = 120 * time.Second
pingPeriod = (pingTimeout * 9) / 10
)
bindString(cmd, &o.Mode,
pFlag(pfix, "pubsub-mode"), "poll",
"Pub/Sub mode (poll, redis")
bindDuration(cmd, &o.RedisPingTimeout,
pFlag(pfix, "pubsub-polling-interval"), timeout,
"Sub/Sub polling interval")
bindString(cmd, &o.RedisAddr,
pFlag(pfix, "pubsub-redis-addr"), "redis:6379",
"Pub/Sub mode (poll, redis")
bindDuration(cmd, &o.RedisTimeout,
pFlag(pfix, "pubsub-redis-timeout"), timeout,
"Websocket connection timeout")
bindDuration(cmd, &o.RedisPingTimeout,
pFlag(pfix, "pubsub-redis-ping-timeout"), pingTimeout,
"Pub/Sub connection ping timeout")
bindDuration(cmd, &o.RedisPingPeriod,
pFlag(pfix, "pubsub-redis-ping-period"), pingPeriod,
"Pub/Sub connection ping period (should be lower than timeout)")
return
}

41
pkg/cli/flags/smtp.go Normal file
View File

@ -0,0 +1,41 @@
package flags
import (
"github.com/spf13/cobra"
)
type (
SMTPOpt struct {
Host string
Port int
User string
Pass string
From string
}
)
func SMTP(cmd *cobra.Command) (o *SMTPOpt) {
o = &SMTPOpt{}
bindString(cmd, &o.Host,
"smtp-host", "localhost:25",
"SMTP hostname")
bindString(cmd, &o.User,
"smtp-username", "",
"SMTP server username")
bindString(cmd, &o.Pass,
"smtp-pass", "",
"SMTP server password")
bindString(cmd, &o.From,
"smtp-from", "",
"Sender's email address")
bindInt(cmd, &o.Port,
"smtp-port", 25,
"SMTP port number")
return
}

View File

@ -0,0 +1,39 @@
package flags
import (
"time"
"github.com/spf13/cobra"
)
type (
WebsocketOpt struct {
Timeout time.Duration
PingTimeout time.Duration
PingPeriod time.Duration
}
)
func Websocket(cmd *cobra.Command, pfix string) (o *WebsocketOpt) {
o = &WebsocketOpt{}
const (
timeout = 15 * time.Second
pingTimeout = 120 * time.Second
pingPeriod = (pingTimeout * 9) / 10
)
bindDuration(cmd, &o.Timeout,
pFlag(pfix, "websocket-timeout"), timeout,
"Websocket connection timeout")
bindDuration(cmd, &o.PingTimeout,
pFlag(pfix, "websocket-ping-timeout"), pingTimeout,
"Websocket connection ping timeout")
bindDuration(cmd, &o.PingPeriod,
pFlag(pfix, "websocket-ping-period"), pingPeriod,
"Websocket connection ping period (should be lower than timeout)")
return
}

107
pkg/cli/helpers.go Normal file
View File

@ -0,0 +1,107 @@
package cli
import (
"context"
"github.com/spf13/cobra"
"go.uber.org/zap"
"github.com/cortezaproject/corteza-server/internal/auth"
"github.com/cortezaproject/corteza-server/internal/http"
"github.com/cortezaproject/corteza-server/internal/logger"
"github.com/cortezaproject/corteza-server/internal/mail"
"github.com/cortezaproject/corteza-server/pkg/cli/flags"
)
// SetupProvisionCommands sets-up standard provision commands
// Deprecated: use SetupProvisionSubCommands
func SetupProvisionCommands(ac func() error, md func() error) *cobra.Command {
var (
cmd = &cobra.Command{
Use: "provision",
Short: "Provision tasks",
}
)
// Add only commands with defined callbacks
if ac != nil {
cmd.AddCommand(&cobra.Command{
Use: "access-control-rules",
Short: "Reset access control rules & roles",
RunE: func(cmd *cobra.Command, args []string) error {
return ac()
},
})
}
// Add only commands with defined callbacks
if md != nil {
cmd.AddCommand(&cobra.Command{
Use: "migrate-database",
Short: "Run database migration scripts",
RunE: func(cmd *cobra.Command, args []string) error {
return md()
},
})
}
return cmd
}
type (
provisioner interface {
ProvisionMigrateDatabase(ctx context.Context) error
ProvisionAccessControl(ctx context.Context) error
}
)
func SetupProvisionSubcommands(ctx context.Context, p provisioner) *cobra.Command {
var (
cmd = &cobra.Command{
Use: "provision",
Short: "Provision tasks",
}
)
// Add only commands with defined callbacks
cmd.AddCommand(&cobra.Command{
Use: "access-control-rules",
Short: "Reset access control rules & roles",
RunE: func(cmd *cobra.Command, args []string) error {
return p.ProvisionAccessControl(ctx)
},
})
// Add only commands with defined callbacks
cmd.AddCommand(&cobra.Command{
Use: "migrate-database",
Short: "Run database migration scripts",
RunE: func(cmd *cobra.Command, args []string) error {
return p.ProvisionMigrateDatabase(ctx)
},
})
return cmd
}
func InitGeneralServices(logOpt *flags.LogOpt, smtpOpt *flags.SMTPOpt, jwtOpt *flags.JWTOpt, httpClientOpt *flags.HttpClientOpt) {
var logLevel = zap.InfoLevel
_ = logLevel.Set(logOpt.Level)
if logger.Default() == nil {
logger.Init(logLevel)
} else {
logger.DefaultLevel.SetLevel(logLevel)
}
auth.SetupDefault(jwtOpt.Secret, jwtOpt.Expiry)
mail.SetupDialer(smtpOpt.Host, smtpOpt.Port, smtpOpt.User, smtpOpt.Pass, smtpOpt.From)
http.SetupDefaults(
httpClientOpt.HttpClientTimeout,
httpClientOpt.ClientTSLInsecure,
)
}

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,70 +0,0 @@
package permit
import (
"bytes"
"context"
"encoding/json"
"net/http"
"github.com/pkg/errors"
)
type (
httpClient interface {
Do(req *http.Request) (*http.Response, error)
}
)
const (
permitCheckEndpoint = "https://permit.crust.tech/check"
)
func Check(ctx context.Context, p Permit) (*Permit, error) {
return CheckWithClient(ctx, http.DefaultClient, p)
}
func CheckWithClient(ctx context.Context, client httpClient, p Permit) (*Permit, error) {
if len(p.Key) == 0 {
return nil, errors.New("key not set")
} else if len(p.Key) != KeyLength {
return nil, errors.Errorf("invalid key length (%d chars)", len(p.Key))
}
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(p); err != nil {
return nil, errors.Wrap(err, "permit encoding failed")
}
if req, err := http.NewRequest("POST", permitCheckEndpoint, buf); err != nil {
return nil, errors.Wrap(err, "unable to create request")
} else {
return CheckWithRequest(client, req.WithContext(ctx))
}
}
func CheckWithRequest(client httpClient, request *http.Request) (p *Permit, err error) {
var rsp *http.Response
if rsp, err = client.Do(request); err != nil {
return nil, errors.Wrap(err, "unable to fetch permit")
}
defer rsp.Body.Close()
switch rsp.StatusCode {
case http.StatusBadRequest:
return nil, errors.New("bad request")
case http.StatusNotFound:
return nil, errors.New("subscription key not found")
case http.StatusInternalServerError:
return nil, errors.New("subscription server error")
case http.StatusUnauthorized:
return nil, errors.New("subscription key invalid")
}
p = &Permit{}
if err = json.NewDecoder(rsp.Body).Decode(&p); err != nil {
return nil, errors.Wrap(err, "unable to decode response into permit")
}
return
}

View File

@ -1,35 +0,0 @@
package permit
import (
"time"
"github.com/pkg/errors"
)
type (
Permit struct {
Version uint `json:"version"`
Key string `json:"key"`
Domain string `json:"domain"`
Expires *time.Time `json:"expires,omitempty"`
Valid bool `json:"valid"`
Attributes map[string]int `json:"attributes"`
}
)
const (
// KeyLength
KeyLength = 64
)
var (
PermitNotFound = errors.New("permit not found")
)
func (p Permit) IsValid() bool {
return p.Valid && !p.Expired()
}
func (p Permit) Expired() bool {
return p.Expires != nil && p.Expires.Before(time.Now())
}

View File

@ -0,0 +1,199 @@
// Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package promhttp
import (
"bufio"
"io"
"net"
"net/http"
)
const (
closeNotifier = 1 << iota
flusher
hijacker
readerFrom
pusher
)
type delegator interface {
http.ResponseWriter
Status() int
Written() int64
}
type responseWriterDelegator struct {
http.ResponseWriter
handler, method string
status int
written int64
wroteHeader bool
observeWriteHeader func(int)
}
func (r *responseWriterDelegator) Status() int {
return r.status
}
func (r *responseWriterDelegator) Written() int64 {
return r.written
}
func (r *responseWriterDelegator) WriteHeader(code int) {
r.status = code
r.wroteHeader = true
r.ResponseWriter.WriteHeader(code)
if r.observeWriteHeader != nil {
r.observeWriteHeader(code)
}
}
func (r *responseWriterDelegator) Write(b []byte) (int, error) {
if !r.wroteHeader {
r.WriteHeader(http.StatusOK)
}
n, err := r.ResponseWriter.Write(b)
r.written += int64(n)
return n, err
}
type closeNotifierDelegator struct{ *responseWriterDelegator }
type flusherDelegator struct{ *responseWriterDelegator }
type hijackerDelegator struct{ *responseWriterDelegator }
type readerFromDelegator struct{ *responseWriterDelegator }
func (d closeNotifierDelegator) CloseNotify() <-chan bool {
return d.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
func (d flusherDelegator) Flush() {
d.ResponseWriter.(http.Flusher).Flush()
}
func (d hijackerDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return d.ResponseWriter.(http.Hijacker).Hijack()
}
func (d readerFromDelegator) ReadFrom(re io.Reader) (int64, error) {
if !d.wroteHeader {
d.WriteHeader(http.StatusOK)
}
n, err := d.ResponseWriter.(io.ReaderFrom).ReadFrom(re)
d.written += n
return n, err
}
var pickDelegator = make([]func(*responseWriterDelegator) delegator, 32)
func init() {
// TODO(beorn7): Code generation would help here.
pickDelegator[0] = func(d *responseWriterDelegator) delegator { // 0
return d
}
pickDelegator[closeNotifier] = func(d *responseWriterDelegator) delegator { // 1
return closeNotifierDelegator{d}
}
pickDelegator[flusher] = func(d *responseWriterDelegator) delegator { // 2
return flusherDelegator{d}
}
pickDelegator[flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 3
return struct {
*responseWriterDelegator
http.Flusher
http.CloseNotifier
}{d, flusherDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[hijacker] = func(d *responseWriterDelegator) delegator { // 4
return hijackerDelegator{d}
}
pickDelegator[hijacker+closeNotifier] = func(d *responseWriterDelegator) delegator { // 5
return struct {
*responseWriterDelegator
http.Hijacker
http.CloseNotifier
}{d, hijackerDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[hijacker+flusher] = func(d *responseWriterDelegator) delegator { // 6
return struct {
*responseWriterDelegator
http.Hijacker
http.Flusher
}{d, hijackerDelegator{d}, flusherDelegator{d}}
}
pickDelegator[hijacker+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 7
return struct {
*responseWriterDelegator
http.Hijacker
http.Flusher
http.CloseNotifier
}{d, hijackerDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[readerFrom] = func(d *responseWriterDelegator) delegator { // 8
return readerFromDelegator{d}
}
pickDelegator[readerFrom+closeNotifier] = func(d *responseWriterDelegator) delegator { // 9
return struct {
*responseWriterDelegator
io.ReaderFrom
http.CloseNotifier
}{d, readerFromDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[readerFrom+flusher] = func(d *responseWriterDelegator) delegator { // 10
return struct {
*responseWriterDelegator
io.ReaderFrom
http.Flusher
}{d, readerFromDelegator{d}, flusherDelegator{d}}
}
pickDelegator[readerFrom+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 11
return struct {
*responseWriterDelegator
io.ReaderFrom
http.Flusher
http.CloseNotifier
}{d, readerFromDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[readerFrom+hijacker] = func(d *responseWriterDelegator) delegator { // 12
return struct {
*responseWriterDelegator
io.ReaderFrom
http.Hijacker
}{d, readerFromDelegator{d}, hijackerDelegator{d}}
}
pickDelegator[readerFrom+hijacker+closeNotifier] = func(d *responseWriterDelegator) delegator { // 13
return struct {
*responseWriterDelegator
io.ReaderFrom
http.Hijacker
http.CloseNotifier
}{d, readerFromDelegator{d}, hijackerDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[readerFrom+hijacker+flusher] = func(d *responseWriterDelegator) delegator { // 14
return struct {
*responseWriterDelegator
io.ReaderFrom
http.Hijacker
http.Flusher
}{d, readerFromDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}}
}
pickDelegator[readerFrom+hijacker+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 15
return struct {
*responseWriterDelegator
io.ReaderFrom
http.Hijacker
http.Flusher
http.CloseNotifier
}{d, readerFromDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}}
}
}

View File

@ -0,0 +1,181 @@
// Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build go1.8
package promhttp
import (
"io"
"net/http"
)
type pusherDelegator struct{ *responseWriterDelegator }
func (d pusherDelegator) Push(target string, opts *http.PushOptions) error {
return d.ResponseWriter.(http.Pusher).Push(target, opts)
}
func init() {
pickDelegator[pusher] = func(d *responseWriterDelegator) delegator { // 16
return pusherDelegator{d}
}
pickDelegator[pusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 17
return struct {
*responseWriterDelegator
http.Pusher
http.CloseNotifier
}{d, pusherDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[pusher+flusher] = func(d *responseWriterDelegator) delegator { // 18
return struct {
*responseWriterDelegator
http.Pusher
http.Flusher
}{d, pusherDelegator{d}, flusherDelegator{d}}
}
pickDelegator[pusher+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 19
return struct {
*responseWriterDelegator
http.Pusher
http.Flusher
http.CloseNotifier
}{d, pusherDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[pusher+hijacker] = func(d *responseWriterDelegator) delegator { // 20
return struct {
*responseWriterDelegator
http.Pusher
http.Hijacker
}{d, pusherDelegator{d}, hijackerDelegator{d}}
}
pickDelegator[pusher+hijacker+closeNotifier] = func(d *responseWriterDelegator) delegator { // 21
return struct {
*responseWriterDelegator
http.Pusher
http.Hijacker
http.CloseNotifier
}{d, pusherDelegator{d}, hijackerDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[pusher+hijacker+flusher] = func(d *responseWriterDelegator) delegator { // 22
return struct {
*responseWriterDelegator
http.Pusher
http.Hijacker
http.Flusher
}{d, pusherDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}}
}
pickDelegator[pusher+hijacker+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { //23
return struct {
*responseWriterDelegator
http.Pusher
http.Hijacker
http.Flusher
http.CloseNotifier
}{d, pusherDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[pusher+readerFrom] = func(d *responseWriterDelegator) delegator { // 24
return struct {
*responseWriterDelegator
http.Pusher
io.ReaderFrom
}{d, pusherDelegator{d}, readerFromDelegator{d}}
}
pickDelegator[pusher+readerFrom+closeNotifier] = func(d *responseWriterDelegator) delegator { // 25
return struct {
*responseWriterDelegator
http.Pusher
io.ReaderFrom
http.CloseNotifier
}{d, pusherDelegator{d}, readerFromDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[pusher+readerFrom+flusher] = func(d *responseWriterDelegator) delegator { // 26
return struct {
*responseWriterDelegator
http.Pusher
io.ReaderFrom
http.Flusher
}{d, pusherDelegator{d}, readerFromDelegator{d}, flusherDelegator{d}}
}
pickDelegator[pusher+readerFrom+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 27
return struct {
*responseWriterDelegator
http.Pusher
io.ReaderFrom
http.Flusher
http.CloseNotifier
}{d, pusherDelegator{d}, readerFromDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[pusher+readerFrom+hijacker] = func(d *responseWriterDelegator) delegator { // 28
return struct {
*responseWriterDelegator
http.Pusher
io.ReaderFrom
http.Hijacker
}{d, pusherDelegator{d}, readerFromDelegator{d}, hijackerDelegator{d}}
}
pickDelegator[pusher+readerFrom+hijacker+closeNotifier] = func(d *responseWriterDelegator) delegator { // 29
return struct {
*responseWriterDelegator
http.Pusher
io.ReaderFrom
http.Hijacker
http.CloseNotifier
}{d, pusherDelegator{d}, readerFromDelegator{d}, hijackerDelegator{d}, closeNotifierDelegator{d}}
}
pickDelegator[pusher+readerFrom+hijacker+flusher] = func(d *responseWriterDelegator) delegator { // 30
return struct {
*responseWriterDelegator
http.Pusher
io.ReaderFrom
http.Hijacker
http.Flusher
}{d, pusherDelegator{d}, readerFromDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}}
}
pickDelegator[pusher+readerFrom+hijacker+flusher+closeNotifier] = func(d *responseWriterDelegator) delegator { // 31
return struct {
*responseWriterDelegator
http.Pusher
io.ReaderFrom
http.Hijacker
http.Flusher
http.CloseNotifier
}{d, pusherDelegator{d}, readerFromDelegator{d}, hijackerDelegator{d}, flusherDelegator{d}, closeNotifierDelegator{d}}
}
}
func newDelegator(w http.ResponseWriter, observeWriteHeaderFunc func(int)) delegator {
d := &responseWriterDelegator{
ResponseWriter: w,
observeWriteHeader: observeWriteHeaderFunc,
}
id := 0
if _, ok := w.(http.CloseNotifier); ok {
id += closeNotifier
}
if _, ok := w.(http.Flusher); ok {
id += flusher
}
if _, ok := w.(http.Hijacker); ok {
id += hijacker
}
if _, ok := w.(io.ReaderFrom); ok {
id += readerFrom
}
if _, ok := w.(http.Pusher); ok {
id += pusher
}
return pickDelegator[id](d)
}

View File

@ -0,0 +1,44 @@
// Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !go1.8
package promhttp
import (
"io"
"net/http"
)
func newDelegator(w http.ResponseWriter, observeWriteHeaderFunc func(int)) delegator {
d := &responseWriterDelegator{
ResponseWriter: w,
observeWriteHeader: observeWriteHeaderFunc,
}
id := 0
if _, ok := w.(http.CloseNotifier); ok {
id += closeNotifier
}
if _, ok := w.(http.Flusher); ok {
id += flusher
}
if _, ok := w.(http.Hijacker); ok {
id += hijacker
}
if _, ok := w.(io.ReaderFrom); ok {
id += readerFrom
}
return pickDelegator[id](d)
}

View File

@ -0,0 +1,311 @@
// Copyright 2016 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package promhttp provides tooling around HTTP servers and clients.
//
// First, the package allows the creation of http.Handler instances to expose
// Prometheus metrics via HTTP. promhttp.Handler acts on the
// prometheus.DefaultGatherer. With HandlerFor, you can create a handler for a
// custom registry or anything that implements the Gatherer interface. It also
// allows the creation of handlers that act differently on errors or allow to
// log errors.
//
// Second, the package provides tooling to instrument instances of http.Handler
// via middleware. Middleware wrappers follow the naming scheme
// InstrumentHandlerX, where X describes the intended use of the middleware.
// See each function's doc comment for specific details.
//
// Finally, the package allows for an http.RoundTripper to be instrumented via
// middleware. Middleware wrappers follow the naming scheme
// InstrumentRoundTripperX, where X describes the intended use of the
// middleware. See each function's doc comment for specific details.
package promhttp
import (
"compress/gzip"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/prometheus/common/expfmt"
"github.com/prometheus/client_golang/prometheus"
)
const (
contentTypeHeader = "Content-Type"
contentLengthHeader = "Content-Length"
contentEncodingHeader = "Content-Encoding"
acceptEncodingHeader = "Accept-Encoding"
)
var gzipPool = sync.Pool{
New: func() interface{} {
return gzip.NewWriter(nil)
},
}
// Handler returns an http.Handler for the prometheus.DefaultGatherer, using
// default HandlerOpts, i.e. it reports the first error as an HTTP error, it has
// no error logging, and it applies compression if requested by the client.
//
// The returned http.Handler is already instrumented using the
// InstrumentMetricHandler function and the prometheus.DefaultRegisterer. If you
// create multiple http.Handlers by separate calls of the Handler function, the
// metrics used for instrumentation will be shared between them, providing
// global scrape counts.
//
// This function is meant to cover the bulk of basic use cases. If you are doing
// anything that requires more customization (including using a non-default
// Gatherer, different instrumentation, and non-default HandlerOpts), use the
// HandlerFor function. See there for details.
func Handler() http.Handler {
return InstrumentMetricHandler(
prometheus.DefaultRegisterer, HandlerFor(prometheus.DefaultGatherer, HandlerOpts{}),
)
}
// HandlerFor returns an uninstrumented http.Handler for the provided
// Gatherer. The behavior of the Handler is defined by the provided
// HandlerOpts. Thus, HandlerFor is useful to create http.Handlers for custom
// Gatherers, with non-default HandlerOpts, and/or with custom (or no)
// instrumentation. Use the InstrumentMetricHandler function to apply the same
// kind of instrumentation as it is used by the Handler function.
func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
var inFlightSem chan struct{}
if opts.MaxRequestsInFlight > 0 {
inFlightSem = make(chan struct{}, opts.MaxRequestsInFlight)
}
h := http.HandlerFunc(func(rsp http.ResponseWriter, req *http.Request) {
if inFlightSem != nil {
select {
case inFlightSem <- struct{}{}: // All good, carry on.
defer func() { <-inFlightSem }()
default:
http.Error(rsp, fmt.Sprintf(
"Limit of concurrent requests reached (%d), try again later.", opts.MaxRequestsInFlight,
), http.StatusServiceUnavailable)
return
}
}
mfs, err := reg.Gather()
if err != nil {
if opts.ErrorLog != nil {
opts.ErrorLog.Println("error gathering metrics:", err)
}
switch opts.ErrorHandling {
case PanicOnError:
panic(err)
case ContinueOnError:
if len(mfs) == 0 {
// Still report the error if no metrics have been gathered.
httpError(rsp, err)
return
}
case HTTPErrorOnError:
httpError(rsp, err)
return
}
}
contentType := expfmt.Negotiate(req.Header)
header := rsp.Header()
header.Set(contentTypeHeader, string(contentType))
w := io.Writer(rsp)
if !opts.DisableCompression && gzipAccepted(req.Header) {
header.Set(contentEncodingHeader, "gzip")
gz := gzipPool.Get().(*gzip.Writer)
defer gzipPool.Put(gz)
gz.Reset(w)
defer gz.Close()
w = gz
}
enc := expfmt.NewEncoder(w, contentType)
var lastErr error
for _, mf := range mfs {
if err := enc.Encode(mf); err != nil {
lastErr = err
if opts.ErrorLog != nil {
opts.ErrorLog.Println("error encoding and sending metric family:", err)
}
switch opts.ErrorHandling {
case PanicOnError:
panic(err)
case ContinueOnError:
// Handled later.
case HTTPErrorOnError:
httpError(rsp, err)
return
}
}
}
if lastErr != nil {
httpError(rsp, lastErr)
}
})
if opts.Timeout <= 0 {
return h
}
return http.TimeoutHandler(h, opts.Timeout, fmt.Sprintf(
"Exceeded configured timeout of %v.\n",
opts.Timeout,
))
}
// InstrumentMetricHandler is usually used with an http.Handler returned by the
// HandlerFor function. It instruments the provided http.Handler with two
// metrics: A counter vector "promhttp_metric_handler_requests_total" to count
// scrapes partitioned by HTTP status code, and a gauge
// "promhttp_metric_handler_requests_in_flight" to track the number of
// simultaneous scrapes. This function idempotently registers collectors for
// both metrics with the provided Registerer. It panics if the registration
// fails. The provided metrics are useful to see how many scrapes hit the
// monitored target (which could be from different Prometheus servers or other
// scrapers), and how often they overlap (which would result in more than one
// scrape in flight at the same time). Note that the scrapes-in-flight gauge
// will contain the scrape by which it is exposed, while the scrape counter will
// only get incremented after the scrape is complete (as only then the status
// code is known). For tracking scrape durations, use the
// "scrape_duration_seconds" gauge created by the Prometheus server upon each
// scrape.
func InstrumentMetricHandler(reg prometheus.Registerer, handler http.Handler) http.Handler {
cnt := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "promhttp_metric_handler_requests_total",
Help: "Total number of scrapes by HTTP status code.",
},
[]string{"code"},
)
// Initialize the most likely HTTP status codes.
cnt.WithLabelValues("200")
cnt.WithLabelValues("500")
cnt.WithLabelValues("503")
if err := reg.Register(cnt); err != nil {
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
cnt = are.ExistingCollector.(*prometheus.CounterVec)
} else {
panic(err)
}
}
gge := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "promhttp_metric_handler_requests_in_flight",
Help: "Current number of scrapes being served.",
})
if err := reg.Register(gge); err != nil {
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
gge = are.ExistingCollector.(prometheus.Gauge)
} else {
panic(err)
}
}
return InstrumentHandlerCounter(cnt, InstrumentHandlerInFlight(gge, handler))
}
// HandlerErrorHandling defines how a Handler serving metrics will handle
// errors.
type HandlerErrorHandling int
// These constants cause handlers serving metrics to behave as described if
// errors are encountered.
const (
// Serve an HTTP status code 500 upon the first error
// encountered. Report the error message in the body.
HTTPErrorOnError HandlerErrorHandling = iota
// Ignore errors and try to serve as many metrics as possible. However,
// if no metrics can be served, serve an HTTP status code 500 and the
// last error message in the body. Only use this in deliberate "best
// effort" metrics collection scenarios. It is recommended to at least
// log errors (by providing an ErrorLog in HandlerOpts) to not mask
// errors completely.
ContinueOnError
// Panic upon the first error encountered (useful for "crash only" apps).
PanicOnError
)
// Logger is the minimal interface HandlerOpts needs for logging. Note that
// log.Logger from the standard library implements this interface, and it is
// easy to implement by custom loggers, if they don't do so already anyway.
type Logger interface {
Println(v ...interface{})
}
// HandlerOpts specifies options how to serve metrics via an http.Handler. The
// zero value of HandlerOpts is a reasonable default.
type HandlerOpts struct {
// ErrorLog specifies an optional logger for errors collecting and
// serving metrics. If nil, errors are not logged at all.
ErrorLog Logger
// ErrorHandling defines how errors are handled. Note that errors are
// logged regardless of the configured ErrorHandling provided ErrorLog
// is not nil.
ErrorHandling HandlerErrorHandling
// If DisableCompression is true, the handler will never compress the
// response, even if requested by the client.
DisableCompression bool
// The number of concurrent HTTP requests is limited to
// MaxRequestsInFlight. Additional requests are responded to with 503
// Service Unavailable and a suitable message in the body. If
// MaxRequestsInFlight is 0 or negative, no limit is applied.
MaxRequestsInFlight int
// If handling a request takes longer than Timeout, it is responded to
// with 503 ServiceUnavailable and a suitable Message. No timeout is
// applied if Timeout is 0 or negative. Note that with the current
// implementation, reaching the timeout simply ends the HTTP requests as
// described above (and even that only if sending of the body hasn't
// started yet), while the bulk work of gathering all the metrics keeps
// running in the background (with the eventual result to be thrown
// away). Until the implementation is improved, it is recommended to
// implement a separate timeout in potentially slow Collectors.
Timeout time.Duration
}
// gzipAccepted returns whether the client will accept gzip-encoded content.
func gzipAccepted(header http.Header) bool {
a := header.Get(acceptEncodingHeader)
parts := strings.Split(a, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "gzip" || strings.HasPrefix(part, "gzip;") {
return true
}
}
return false
}
// httpError removes any content-encoding header and then calls http.Error with
// the provided error and http.StatusInternalServerErrer. Error contents is
// supposed to be uncompressed plain text. However, same as with a plain
// http.Error, any header settings will be void if the header has already been
// sent. The error message will still be written to the writer, but it will
// probably be of limited use.
func httpError(rsp http.ResponseWriter, err error) {
rsp.Header().Del(contentEncodingHeader)
http.Error(
rsp,
"An error has occurred while serving metrics:\n\n"+err.Error(),
http.StatusInternalServerError,
)
}

View File

@ -0,0 +1,97 @@
// Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package promhttp
import (
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
)
// The RoundTripperFunc type is an adapter to allow the use of ordinary
// functions as RoundTrippers. If f is a function with the appropriate
// signature, RountTripperFunc(f) is a RoundTripper that calls f.
type RoundTripperFunc func(req *http.Request) (*http.Response, error)
// RoundTrip implements the RoundTripper interface.
func (rt RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return rt(r)
}
// InstrumentRoundTripperInFlight is a middleware that wraps the provided
// http.RoundTripper. It sets the provided prometheus.Gauge to the number of
// requests currently handled by the wrapped http.RoundTripper.
//
// See the example for ExampleInstrumentRoundTripperDuration for example usage.
func InstrumentRoundTripperInFlight(gauge prometheus.Gauge, next http.RoundTripper) RoundTripperFunc {
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
gauge.Inc()
defer gauge.Dec()
return next.RoundTrip(r)
})
}
// InstrumentRoundTripperCounter is a middleware that wraps the provided
// http.RoundTripper to observe the request result with the provided CounterVec.
// The CounterVec must have zero, one, or two non-const non-curried labels. For
// those, the only allowed label names are "code" and "method". The function
// panics otherwise. Partitioning of the CounterVec happens by HTTP status code
// and/or HTTP method if the respective instance label names are present in the
// CounterVec. For unpartitioned counting, use a CounterVec with zero labels.
//
// If the wrapped RoundTripper panics or returns a non-nil error, the Counter
// is not incremented.
//
// See the example for ExampleInstrumentRoundTripperDuration for example usage.
func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.RoundTripper) RoundTripperFunc {
code, method := checkLabels(counter)
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
resp, err := next.RoundTrip(r)
if err == nil {
counter.With(labels(code, method, r.Method, resp.StatusCode)).Inc()
}
return resp, err
})
}
// InstrumentRoundTripperDuration is a middleware that wraps the provided
// http.RoundTripper to observe the request duration with the provided
// ObserverVec. The ObserverVec must have zero, one, or two non-const
// non-curried labels. For those, the only allowed label names are "code" and
// "method". The function panics otherwise. The Observe method of the Observer
// in the ObserverVec is called with the request duration in
// seconds. Partitioning happens by HTTP status code and/or HTTP method if the
// respective instance label names are present in the ObserverVec. For
// unpartitioned observations, use an ObserverVec with zero labels. Note that
// partitioning of Histograms is expensive and should be used judiciously.
//
// If the wrapped RoundTripper panics or returns a non-nil error, no values are
// reported.
//
// Note that this method is only guaranteed to never observe negative durations
// if used with Go1.9+.
func InstrumentRoundTripperDuration(obs prometheus.ObserverVec, next http.RoundTripper) RoundTripperFunc {
code, method := checkLabels(obs)
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
start := time.Now()
resp, err := next.RoundTrip(r)
if err == nil {
obs.With(labels(code, method, r.Method, resp.StatusCode)).Observe(time.Since(start).Seconds())
}
return resp, err
})
}

View File

@ -0,0 +1,144 @@
// Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build go1.8
package promhttp
import (
"context"
"crypto/tls"
"net/http"
"net/http/httptrace"
"time"
)
// InstrumentTrace is used to offer flexibility in instrumenting the available
// httptrace.ClientTrace hook functions. Each function is passed a float64
// representing the time in seconds since the start of the http request. A user
// may choose to use separately buckets Histograms, or implement custom
// instance labels on a per function basis.
type InstrumentTrace struct {
GotConn func(float64)
PutIdleConn func(float64)
GotFirstResponseByte func(float64)
Got100Continue func(float64)
DNSStart func(float64)
DNSDone func(float64)
ConnectStart func(float64)
ConnectDone func(float64)
TLSHandshakeStart func(float64)
TLSHandshakeDone func(float64)
WroteHeaders func(float64)
Wait100Continue func(float64)
WroteRequest func(float64)
}
// InstrumentRoundTripperTrace is a middleware that wraps the provided
// RoundTripper and reports times to hook functions provided in the
// InstrumentTrace struct. Hook functions that are not present in the provided
// InstrumentTrace struct are ignored. Times reported to the hook functions are
// time since the start of the request. Only with Go1.9+, those times are
// guaranteed to never be negative. (Earlier Go versions are not using a
// monotonic clock.) Note that partitioning of Histograms is expensive and
// should be used judiciously.
//
// For hook functions that receive an error as an argument, no observations are
// made in the event of a non-nil error value.
//
// See the example for ExampleInstrumentRoundTripperDuration for example usage.
func InstrumentRoundTripperTrace(it *InstrumentTrace, next http.RoundTripper) RoundTripperFunc {
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
start := time.Now()
trace := &httptrace.ClientTrace{
GotConn: func(_ httptrace.GotConnInfo) {
if it.GotConn != nil {
it.GotConn(time.Since(start).Seconds())
}
},
PutIdleConn: func(err error) {
if err != nil {
return
}
if it.PutIdleConn != nil {
it.PutIdleConn(time.Since(start).Seconds())
}
},
DNSStart: func(_ httptrace.DNSStartInfo) {
if it.DNSStart != nil {
it.DNSStart(time.Since(start).Seconds())
}
},
DNSDone: func(_ httptrace.DNSDoneInfo) {
if it.DNSDone != nil {
it.DNSDone(time.Since(start).Seconds())
}
},
ConnectStart: func(_, _ string) {
if it.ConnectStart != nil {
it.ConnectStart(time.Since(start).Seconds())
}
},
ConnectDone: func(_, _ string, err error) {
if err != nil {
return
}
if it.ConnectDone != nil {
it.ConnectDone(time.Since(start).Seconds())
}
},
GotFirstResponseByte: func() {
if it.GotFirstResponseByte != nil {
it.GotFirstResponseByte(time.Since(start).Seconds())
}
},
Got100Continue: func() {
if it.Got100Continue != nil {
it.Got100Continue(time.Since(start).Seconds())
}
},
TLSHandshakeStart: func() {
if it.TLSHandshakeStart != nil {
it.TLSHandshakeStart(time.Since(start).Seconds())
}
},
TLSHandshakeDone: func(_ tls.ConnectionState, err error) {
if err != nil {
return
}
if it.TLSHandshakeDone != nil {
it.TLSHandshakeDone(time.Since(start).Seconds())
}
},
WroteHeaders: func() {
if it.WroteHeaders != nil {
it.WroteHeaders(time.Since(start).Seconds())
}
},
Wait100Continue: func() {
if it.Wait100Continue != nil {
it.Wait100Continue(time.Since(start).Seconds())
}
},
WroteRequest: func(_ httptrace.WroteRequestInfo) {
if it.WroteRequest != nil {
it.WroteRequest(time.Since(start).Seconds())
}
},
}
r = r.WithContext(httptrace.WithClientTrace(context.Background(), trace))
return next.RoundTrip(r)
})
}

View File

@ -0,0 +1,447 @@
// Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package promhttp
import (
"errors"
"net/http"
"strconv"
"strings"
"time"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/client_golang/prometheus"
)
// magicString is used for the hacky label test in checkLabels. Remove once fixed.
const magicString = "zZgWfBxLqvG8kc8IMv3POi2Bb0tZI3vAnBx+gBaFi9FyPzB/CzKUer1yufDa"
// InstrumentHandlerInFlight is a middleware that wraps the provided
// http.Handler. It sets the provided prometheus.Gauge to the number of
// requests currently handled by the wrapped http.Handler.
//
// See the example for InstrumentHandlerDuration for example usage.
func InstrumentHandlerInFlight(g prometheus.Gauge, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
g.Inc()
defer g.Dec()
next.ServeHTTP(w, r)
})
}
// InstrumentHandlerDuration is a middleware that wraps the provided
// http.Handler to observe the request duration with the provided ObserverVec.
// The ObserverVec must have zero, one, or two non-const non-curried labels. For
// those, the only allowed label names are "code" and "method". The function
// panics otherwise. The Observe method of the Observer in the ObserverVec is
// called with the request duration in seconds. Partitioning happens by HTTP
// status code and/or HTTP method if the respective instance label names are
// present in the ObserverVec. For unpartitioned observations, use an
// ObserverVec with zero labels. Note that partitioning of Histograms is
// expensive and should be used judiciously.
//
// If the wrapped Handler does not set a status code, a status code of 200 is assumed.
//
// If the wrapped Handler panics, no values are reported.
//
// Note that this method is only guaranteed to never observe negative durations
// if used with Go1.9+.
func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler) http.HandlerFunc {
code, method := checkLabels(obs)
if code {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
d := newDelegator(w, nil)
next.ServeHTTP(d, r)
obs.With(labels(code, method, r.Method, d.Status())).Observe(time.Since(now).Seconds())
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
next.ServeHTTP(w, r)
obs.With(labels(code, method, r.Method, 0)).Observe(time.Since(now).Seconds())
})
}
// InstrumentHandlerCounter is a middleware that wraps the provided http.Handler
// to observe the request result with the provided CounterVec. The CounterVec
// must have zero, one, or two non-const non-curried labels. For those, the only
// allowed label names are "code" and "method". The function panics
// otherwise. Partitioning of the CounterVec happens by HTTP status code and/or
// HTTP method if the respective instance label names are present in the
// CounterVec. For unpartitioned counting, use a CounterVec with zero labels.
//
// If the wrapped Handler does not set a status code, a status code of 200 is assumed.
//
// If the wrapped Handler panics, the Counter is not incremented.
//
// See the example for InstrumentHandlerDuration for example usage.
func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler) http.HandlerFunc {
code, method := checkLabels(counter)
if code {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
d := newDelegator(w, nil)
next.ServeHTTP(d, r)
counter.With(labels(code, method, r.Method, d.Status())).Inc()
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
counter.With(labels(code, method, r.Method, 0)).Inc()
})
}
// InstrumentHandlerTimeToWriteHeader is a middleware that wraps the provided
// http.Handler to observe with the provided ObserverVec the request duration
// until the response headers are written. The ObserverVec must have zero, one,
// or two non-const non-curried labels. For those, the only allowed label names
// are "code" and "method". The function panics otherwise. The Observe method of
// the Observer in the ObserverVec is called with the request duration in
// seconds. Partitioning happens by HTTP status code and/or HTTP method if the
// respective instance label names are present in the ObserverVec. For
// unpartitioned observations, use an ObserverVec with zero labels. Note that
// partitioning of Histograms is expensive and should be used judiciously.
//
// If the wrapped Handler panics before calling WriteHeader, no value is
// reported.
//
// Note that this method is only guaranteed to never observe negative durations
// if used with Go1.9+.
//
// See the example for InstrumentHandlerDuration for example usage.
func InstrumentHandlerTimeToWriteHeader(obs prometheus.ObserverVec, next http.Handler) http.HandlerFunc {
code, method := checkLabels(obs)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
d := newDelegator(w, func(status int) {
obs.With(labels(code, method, r.Method, status)).Observe(time.Since(now).Seconds())
})
next.ServeHTTP(d, r)
})
}
// InstrumentHandlerRequestSize is a middleware that wraps the provided
// http.Handler to observe the request size with the provided ObserverVec. The
// ObserverVec must have zero, one, or two non-const non-curried labels. For
// those, the only allowed label names are "code" and "method". The function
// panics otherwise. The Observe method of the Observer in the ObserverVec is
// called with the request size in bytes. Partitioning happens by HTTP status
// code and/or HTTP method if the respective instance label names are present in
// the ObserverVec. For unpartitioned observations, use an ObserverVec with zero
// labels. Note that partitioning of Histograms is expensive and should be used
// judiciously.
//
// If the wrapped Handler does not set a status code, a status code of 200 is assumed.
//
// If the wrapped Handler panics, no values are reported.
//
// See the example for InstrumentHandlerDuration for example usage.
func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler) http.HandlerFunc {
code, method := checkLabels(obs)
if code {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
d := newDelegator(w, nil)
next.ServeHTTP(d, r)
size := computeApproximateRequestSize(r)
obs.With(labels(code, method, r.Method, d.Status())).Observe(float64(size))
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
size := computeApproximateRequestSize(r)
obs.With(labels(code, method, r.Method, 0)).Observe(float64(size))
})
}
// InstrumentHandlerResponseSize is a middleware that wraps the provided
// http.Handler to observe the response size with the provided ObserverVec. The
// ObserverVec must have zero, one, or two non-const non-curried labels. For
// those, the only allowed label names are "code" and "method". The function
// panics otherwise. The Observe method of the Observer in the ObserverVec is
// called with the response size in bytes. Partitioning happens by HTTP status
// code and/or HTTP method if the respective instance label names are present in
// the ObserverVec. For unpartitioned observations, use an ObserverVec with zero
// labels. Note that partitioning of Histograms is expensive and should be used
// judiciously.
//
// If the wrapped Handler does not set a status code, a status code of 200 is assumed.
//
// If the wrapped Handler panics, no values are reported.
//
// See the example for InstrumentHandlerDuration for example usage.
func InstrumentHandlerResponseSize(obs prometheus.ObserverVec, next http.Handler) http.Handler {
code, method := checkLabels(obs)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
d := newDelegator(w, nil)
next.ServeHTTP(d, r)
obs.With(labels(code, method, r.Method, d.Status())).Observe(float64(d.Written()))
})
}
func checkLabels(c prometheus.Collector) (code bool, method bool) {
// TODO(beorn7): Remove this hacky way to check for instance labels
// once Descriptors can have their dimensionality queried.
var (
desc *prometheus.Desc
m prometheus.Metric
pm dto.Metric
lvs []string
)
// Get the Desc from the Collector.
descc := make(chan *prometheus.Desc, 1)
c.Describe(descc)
select {
case desc = <-descc:
default:
panic("no description provided by collector")
}
select {
case <-descc:
panic("more than one description provided by collector")
default:
}
close(descc)
// Create a ConstMetric with the Desc. Since we don't know how many
// variable labels there are, try for as long as it needs.
for err := errors.New("dummy"); err != nil; lvs = append(lvs, magicString) {
m, err = prometheus.NewConstMetric(desc, prometheus.UntypedValue, 0, lvs...)
}
// Write out the metric into a proto message and look at the labels.
// If the value is not the magicString, it is a constLabel, which doesn't interest us.
// If the label is curried, it doesn't interest us.
// In all other cases, only "code" or "method" is allowed.
if err := m.Write(&pm); err != nil {
panic("error checking metric for labels")
}
for _, label := range pm.Label {
name, value := label.GetName(), label.GetValue()
if value != magicString || isLabelCurried(c, name) {
continue
}
switch name {
case "code":
code = true
case "method":
method = true
default:
panic("metric partitioned with non-supported labels")
}
}
return
}
func isLabelCurried(c prometheus.Collector, label string) bool {
// This is even hackier than the label test above.
// We essentially try to curry again and see if it works.
// But for that, we need to type-convert to the two
// types we use here, ObserverVec or *CounterVec.
switch v := c.(type) {
case *prometheus.CounterVec:
if _, err := v.CurryWith(prometheus.Labels{label: "dummy"}); err == nil {
return false
}
case prometheus.ObserverVec:
if _, err := v.CurryWith(prometheus.Labels{label: "dummy"}); err == nil {
return false
}
default:
panic("unsupported metric vec type")
}
return true
}
// emptyLabels is a one-time allocation for non-partitioned metrics to avoid
// unnecessary allocations on each request.
var emptyLabels = prometheus.Labels{}
func labels(code, method bool, reqMethod string, status int) prometheus.Labels {
if !(code || method) {
return emptyLabels
}
labels := prometheus.Labels{}
if code {
labels["code"] = sanitizeCode(status)
}
if method {
labels["method"] = sanitizeMethod(reqMethod)
}
return labels
}
func computeApproximateRequestSize(r *http.Request) int {
s := 0
if r.URL != nil {
s += len(r.URL.String())
}
s += len(r.Method)
s += len(r.Proto)
for name, values := range r.Header {
s += len(name)
for _, value := range values {
s += len(value)
}
}
s += len(r.Host)
// N.B. r.Form and r.MultipartForm are assumed to be included in r.URL.
if r.ContentLength != -1 {
s += int(r.ContentLength)
}
return s
}
func sanitizeMethod(m string) string {
switch m {
case "GET", "get":
return "get"
case "PUT", "put":
return "put"
case "HEAD", "head":
return "head"
case "POST", "post":
return "post"
case "DELETE", "delete":
return "delete"
case "CONNECT", "connect":
return "connect"
case "OPTIONS", "options":
return "options"
case "NOTIFY", "notify":
return "notify"
default:
return strings.ToLower(m)
}
}
// If the wrapped http.Handler has not set a status code, i.e. the value is
// currently 0, santizeCode will return 200, for consistency with behavior in
// the stdlib.
func sanitizeCode(s int) string {
switch s {
case 100:
return "100"
case 101:
return "101"
case 200, 0:
return "200"
case 201:
return "201"
case 202:
return "202"
case 203:
return "203"
case 204:
return "204"
case 205:
return "205"
case 206:
return "206"
case 300:
return "300"
case 301:
return "301"
case 302:
return "302"
case 304:
return "304"
case 305:
return "305"
case 307:
return "307"
case 400:
return "400"
case 401:
return "401"
case 402:
return "402"
case 403:
return "403"
case 404:
return "404"
case 405:
return "405"
case 406:
return "406"
case 407:
return "407"
case 408:
return "408"
case 409:
return "409"
case 410:
return "410"
case 411:
return "411"
case 412:
return "412"
case 413:
return "413"
case 414:
return "414"
case 415:
return "415"
case 416:
return "416"
case 417:
return "417"
case 418:
return "418"
case 500:
return "500"
case 501:
return "501"
case 502:
return "502"
case 503:
return "503"
case 504:
return "504"
case 505:
return "505"
case 428:
return "428"
case 429:
return "429"
case 431:
return "431"
case 511:
return "511"
default:
return strconv.Itoa(s)
}
}

25
vendor/github.com/spf13/cast/.gitignore generated vendored Normal file
View File

@ -0,0 +1,25 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.bench

15
vendor/github.com/spf13/cast/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,15 @@
language: go
env:
- GO111MODULE=on
sudo: required
go:
- "1.11.x"
- tip
os:
- linux
matrix:
allow_failures:
- go: tip
fast_finish: true
script:
- make check

21
vendor/github.com/spf13/cast/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Steve Francia
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

38
vendor/github.com/spf13/cast/Makefile generated vendored Normal file
View File

@ -0,0 +1,38 @@
# A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
.PHONY: check fmt lint test test-race vet test-cover-html help
.DEFAULT_GOAL := help
check: test-race fmt vet lint ## Run tests and linters
test: ## Run tests
go test ./...
test-race: ## Run tests with race detector
go test -race ./...
fmt: ## Run gofmt linter
@for d in `go list` ; do \
if [ "`gofmt -l -s $$GOPATH/src/$$d | tee /dev/stderr`" ]; then \
echo "^ improperly formatted go files" && echo && exit 1; \
fi \
done
lint: ## Run golint linter
@for d in `go list` ; do \
if [ "`golint $$d | tee /dev/stderr`" ]; then \
echo "^ golint errors!" && echo && exit 1; \
fi \
done
vet: ## Run go vet linter
@if [ "`go vet | tee /dev/stderr`" ]; then \
echo "^ go vet errors!" && echo && exit 1; \
fi
test-cover-html: ## Generate test coverage report
go test -coverprofile=coverage.out -covermode=count
go tool cover -func=coverage.out
help:
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

75
vendor/github.com/spf13/cast/README.md generated vendored Normal file
View File

@ -0,0 +1,75 @@
cast
====
[![GoDoc](https://godoc.org/github.com/spf13/cast?status.svg)](https://godoc.org/github.com/spf13/cast)
[![Build Status](https://api.travis-ci.org/spf13/cast.svg?branch=master)](https://travis-ci.org/spf13/cast)
[![Go Report Card](https://goreportcard.com/badge/github.com/spf13/cast)](https://goreportcard.com/report/github.com/spf13/cast)
Easy and safe casting from one type to another in Go
Dont Panic! ... Cast
## What is Cast?
Cast is a library to convert between different go types in a consistent and easy way.
Cast provides simple functions to easily convert a number to a string, an
interface into a bool, etc. Cast does this intelligently when an obvious
conversion is possible. It doesnt make any attempts to guess what you meant,
for example you can only convert a string to an int when it is a string
representation of an int such as “8”. Cast was developed for use in
[Hugo](http://hugo.spf13.com), a website engine which uses YAML, TOML or JSON
for meta data.
## Why use Cast?
When working with dynamic data in Go you often need to cast or convert the data
from one type into another. Cast goes beyond just using type assertion (though
it uses that when possible) to provide a very straightforward and convenient
library.
If you are working with interfaces to handle things like dynamic content
youll need an easy way to convert an interface into a given type. This
is the library for you.
If you are taking in data from YAML, TOML or JSON or other formats which lack
full types, then Cast is the library for you.
## Usage
Cast provides a handful of To_____ methods. These methods will always return
the desired type. **If input is provided that will not convert to that type, the
0 or nil value for that type will be returned**.
Cast also provides identical methods To_____E. These return the same result as
the To_____ methods, plus an additional error which tells you if it successfully
converted. Using these methods you can tell the difference between when the
input matched the zero value or when the conversion failed and the zero value
was returned.
The following examples are merely a sample of what is available. Please review
the code for a complete set.
### Example ToString:
cast.ToString("mayonegg") // "mayonegg"
cast.ToString(8) // "8"
cast.ToString(8.31) // "8.31"
cast.ToString([]byte("one time")) // "one time"
cast.ToString(nil) // ""
var foo interface{} = "one more time"
cast.ToString(foo) // "one more time"
### Example ToInt:
cast.ToInt(8) // 8
cast.ToInt(8.31) // 8
cast.ToInt("8") // 8
cast.ToInt(true) // 1
cast.ToInt(false) // 0
var eight interface{} = 8
cast.ToInt(eight) // 8
cast.ToInt(nil) // 0

171
vendor/github.com/spf13/cast/cast.go generated vendored Normal file
View File

@ -0,0 +1,171 @@
// Copyright © 2014 Steve Francia <spf@spf13.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// Package cast provides easy and safe casting in Go.
package cast
import "time"
// ToBool casts an interface to a bool type.
func ToBool(i interface{}) bool {
v, _ := ToBoolE(i)
return v
}
// ToTime casts an interface to a time.Time type.
func ToTime(i interface{}) time.Time {
v, _ := ToTimeE(i)
return v
}
// ToDuration casts an interface to a time.Duration type.
func ToDuration(i interface{}) time.Duration {
v, _ := ToDurationE(i)
return v
}
// ToFloat64 casts an interface to a float64 type.
func ToFloat64(i interface{}) float64 {
v, _ := ToFloat64E(i)
return v
}
// ToFloat32 casts an interface to a float32 type.
func ToFloat32(i interface{}) float32 {
v, _ := ToFloat32E(i)
return v
}
// ToInt64 casts an interface to an int64 type.
func ToInt64(i interface{}) int64 {
v, _ := ToInt64E(i)
return v
}
// ToInt32 casts an interface to an int32 type.
func ToInt32(i interface{}) int32 {
v, _ := ToInt32E(i)
return v
}
// ToInt16 casts an interface to an int16 type.
func ToInt16(i interface{}) int16 {
v, _ := ToInt16E(i)
return v
}
// ToInt8 casts an interface to an int8 type.
func ToInt8(i interface{}) int8 {
v, _ := ToInt8E(i)
return v
}
// ToInt casts an interface to an int type.
func ToInt(i interface{}) int {
v, _ := ToIntE(i)
return v
}
// ToUint casts an interface to a uint type.
func ToUint(i interface{}) uint {
v, _ := ToUintE(i)
return v
}
// ToUint64 casts an interface to a uint64 type.
func ToUint64(i interface{}) uint64 {
v, _ := ToUint64E(i)
return v
}
// ToUint32 casts an interface to a uint32 type.
func ToUint32(i interface{}) uint32 {
v, _ := ToUint32E(i)
return v
}
// ToUint16 casts an interface to a uint16 type.
func ToUint16(i interface{}) uint16 {
v, _ := ToUint16E(i)
return v
}
// ToUint8 casts an interface to a uint8 type.
func ToUint8(i interface{}) uint8 {
v, _ := ToUint8E(i)
return v
}
// ToString casts an interface to a string type.
func ToString(i interface{}) string {
v, _ := ToStringE(i)
return v
}
// ToStringMapString casts an interface to a map[string]string type.
func ToStringMapString(i interface{}) map[string]string {
v, _ := ToStringMapStringE(i)
return v
}
// ToStringMapStringSlice casts an interface to a map[string][]string type.
func ToStringMapStringSlice(i interface{}) map[string][]string {
v, _ := ToStringMapStringSliceE(i)
return v
}
// ToStringMapBool casts an interface to a map[string]bool type.
func ToStringMapBool(i interface{}) map[string]bool {
v, _ := ToStringMapBoolE(i)
return v
}
// ToStringMapInt casts an interface to a map[string]int type.
func ToStringMapInt(i interface{}) map[string]int {
v, _ := ToStringMapIntE(i)
return v
}
// ToStringMapInt64 casts an interface to a map[string]int64 type.
func ToStringMapInt64(i interface{}) map[string]int64 {
v, _ := ToStringMapInt64E(i)
return v
}
// ToStringMap casts an interface to a map[string]interface{} type.
func ToStringMap(i interface{}) map[string]interface{} {
v, _ := ToStringMapE(i)
return v
}
// ToSlice casts an interface to a []interface{} type.
func ToSlice(i interface{}) []interface{} {
v, _ := ToSliceE(i)
return v
}
// ToBoolSlice casts an interface to a []bool type.
func ToBoolSlice(i interface{}) []bool {
v, _ := ToBoolSliceE(i)
return v
}
// ToStringSlice casts an interface to a []string type.
func ToStringSlice(i interface{}) []string {
v, _ := ToStringSliceE(i)
return v
}
// ToIntSlice casts an interface to a []int type.
func ToIntSlice(i interface{}) []int {
v, _ := ToIntSliceE(i)
return v
}
// ToDurationSlice casts an interface to a []time.Duration type.
func ToDurationSlice(i interface{}) []time.Duration {
v, _ := ToDurationSliceE(i)
return v
}

1249
vendor/github.com/spf13/cast/caste.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

7
vendor/github.com/spf13/cast/go.mod generated vendored Normal file
View File

@ -0,0 +1,7 @@
module github.com/spf13/cast
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2
)

6
vendor/github.com/spf13/cast/go.sum generated vendored Normal file
View File

@ -0,0 +1,6 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=