diff --git a/Dockerfile b/Dockerfile index d28c748..23d036d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,11 @@ FROM scratch +ENV K8SAPP_LOCAL_HOST 0.0.0.0 +ENV K8SAPP_LOCAL_PORT 8080 +ENV K8SAPP_LOG_LEVEL 0 + +EXPOSE $K8SAPP_LOCAL_PORT + COPY certs /etc/ssl/ COPY bin/linux-amd64/k8sapp / diff --git a/Gopkg.lock b/Gopkg.lock index 3932761..8a6d864 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,16 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + name = "github.com/julienschmidt/httprouter" + packages = ["."] + revision = "975b5c4c7c21c0e3d2764200bf2aa8e34657ae6e" + +[[projects]] + name = "github.com/kelseyhightower/envconfig" + packages = ["."] + revision = "70f0258d44cbaa3b6a2581d82f58da01a38e4de4" + [[projects]] name = "github.com/rs/xhandler" packages = ["."] @@ -45,6 +55,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "73486fb5324297d10241203b4e59c82f13ebffb4e47b02e14b5da8182ac86eb4" + inputs-digest = "e59ccb0cbb49d6ee79d6526fbc5be075343a1f24c46c3aa2c74d1e6c89f09ece" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index fc6f200..1e242ce 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -28,3 +28,11 @@ [[constraint]] name = "github.com/sirupsen/logrus" version = "1.0.3" + +[[constraint]] + name = "github.com/julienschmidt/httprouter" + revision = "975b5c4c7c21c0e3d2764200bf2aa8e34657ae6e" + +[[constraint]] + name = "github.com/kelseyhightower/envconfig" + revision = "70f0258d44cbaa3b6a2581d82f58da01a38e4de4" diff --git a/Makefile b/Makefile index af5e62b..d82c8dd 100644 --- a/Makefile +++ b/Makefile @@ -5,15 +5,20 @@ APP=k8sapp PROJECT=github.com/takama/k8sapp REGISTRY?=docker.io/takama -CONTAINER_IMAGE?=${REGISTRY}/${APP} -CONTAINER_NAME?=${APP} CA_DIR?=certs # Use the 0.0.0 tag for testing, it shouldn't clobber any release builds -RELEASE?=0.2.2 +RELEASE?=0.3.0 GOOS?=linux GOARCH?=amd64 +K8SAPP_LOCAL_HOST?=0.0.0.0 +K8SAPP_LOCAL_PORT?=8080 +K8SAPP_LOG_LEVEL?=0 + +CONTAINER_IMAGE?=${REGISTRY}/${APP} +CONTAINER_NAME?=${APP} + REPO_INFO=$(shell git config --get remote.origin.url) ifndef COMMIT @@ -55,7 +60,10 @@ push: build .PHONY: run run: build @echo "+ $@" - @docker run --name ${CONTAINER_NAME} \ + @docker run --name ${CONTAINER_NAME} -p ${K8SAPP_LOCAL_PORT}:${K8SAPP_LOCAL_PORT} \ + -e "K8SAPP_LOCAL_HOST=${K8SAPP_LOCAL_HOST}" \ + -e "K8SAPP_LOCAL_PORT=${K8SAPP_LOCAL_PORT}" \ + -e "K8SAPP_LOG_LEVEL=${K8SAPP_LOG_LEVEL}" \ -d $(CONTAINER_IMAGE):$(RELEASE) @sleep 1 @docker logs ${CONTAINER_NAME} diff --git a/README.md b/README.md index a164171..5b8d8b6 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,15 @@ type Logger interface { Just make your choice ```go -func New(cfg *Config) Logger { - // return newLogrus(cfg) - // return newXLog(cfg) - return newStdLog(cfg) +func Run() (err error) { + // log := xlog.New() + // log := logrus.New() + log := stdlog.New(&logger.Config{ + Level: logger.LevelDebug, + Time: true, + UTC: true, + }) + ... } ``` diff --git a/cmd/k8sapp.go b/cmd/k8sapp.go index e43c759..234d014 100644 --- a/cmd/k8sapp.go +++ b/cmd/k8sapp.go @@ -5,13 +5,26 @@ package main import ( + "fmt" "log" + "github.com/takama/k8sapp/pkg/config" "github.com/takama/k8sapp/pkg/service" ) func main() { - if err := service.Run(); err != nil { + // Load ENV configuration + cfg := new(config.Config) + if err := cfg.Load(config.SERVICENAME); err != nil { log.Fatal(err) } + + // Configure service and get router + router, err := service.Setup(cfg) + if err != nil { + log.Fatal(err) + } + + // Listen and serve handlers + router.Listen(fmt.Sprintf("%s:%d", cfg.LocalHost, cfg.LocalPort)) } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3231376..9db1141 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,7 +1,23 @@ -# Version 0.2.2 +# Version 0.3.0 [Documentation](README.md) +## Changelog since 0.2.2 + +### Documentation + +- Added usage description of the loggers: [xlog](https://github.com/rs/xlog), [logrus](https://github.com/sirupsen/logrus) + +### Codebase + +- Added routers interface ([#22](https://github.com/takama/k8sapp/pull/22), [@takama](https://github.com/takama)) +- Implemented Bit-Route interface ([#23](https://github.com/takama/k8sapp/pull/23), [@takama](https://github.com/takama)) +- Implemented httprouter interface ([#24](https://github.com/takama/k8sapp/pull/24), [@takama](https://github.com/takama)) +- Added environment configuration ([#26](https://github.com/takama/k8sapp/pull/26), [@takama](https://github.com/takama)) +- Refactoring of the packages relations ([#25](https://github.com/takama/k8sapp/pull/25), [@takama](https://github.com/takama)) +- Added health/ready handlers ([#27](https://github.com/takama/k8sapp/pull/27), [@takama](https://github.com/takama)) + + ## Changelog since 0.2.1 ### Tests diff --git a/pkg/config/config.go b/pkg/config/config.go index f25f228..0351ac2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,7 +4,27 @@ package config +import ( + "github.com/kelseyhightower/envconfig" + "github.com/takama/k8sapp/pkg/logger" +) + const ( // SERVICENAME contains a service name prefix which used in ENV variables SERVICENAME = "K8SAPP" ) + +// Config contains ENV variables +type Config struct { + // Local service host + LocalHost string `split_words:"true"` + // Local service port + LocalPort int `split_words:"true"` + // Logging level in logger.Level notation + LogLevel logger.Level `split_words:"true"` +} + +// Load settles ENV variables into Config structure +func (c *Config) Load(serviceName string) error { + return envconfig.Process(serviceName, c) +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..5e46ecf --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,11 @@ +package config + +import "testing" + +func TestLoadConfig(t *testing.T) { + config := new(Config) + err := config.Load(SERVICENAME) + if err != nil { + t.Error("Expected loading of environment vars, got", err) + } +} diff --git a/pkg/handlers/handler.go b/pkg/handlers/handler.go new file mode 100644 index 0000000..eb5f923 --- /dev/null +++ b/pkg/handlers/handler.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "fmt" + "net/http" + + "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" + "github.com/takama/k8sapp/pkg/router" + "github.com/takama/k8sapp/pkg/version" +) + +// Handler defines common part for all handlers +type Handler struct { + logger logger.Logger + config *config.Config +} + +// New returns new instance of the Handler +func New(logger logger.Logger, config *config.Config) *Handler { + return &Handler{ + logger: logger, + config: config, + } +} + +// Base handler implements middleware logic +func (h *Handler) Base(handle func(router.Control)) func(router.Control) { + return func(c router.Control) { + + // TODO: Add custom logic here + + handle(c) + } +} + +// Root handler shows version +func (h *Handler) Root(c router.Control) { + c.Code(http.StatusOK) + c.Write(fmt.Sprintf("%s v%s", config.SERVICENAME, version.RELEASE)) +} diff --git a/pkg/handlers/handler_test.go b/pkg/handlers/handler_test.go new file mode 100644 index 0000000..e2cb809 --- /dev/null +++ b/pkg/handlers/handler_test.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" + "github.com/takama/k8sapp/pkg/logger/standard" + "github.com/takama/k8sapp/pkg/router/bitroute" + "github.com/takama/k8sapp/pkg/version" +) + +func TestRoot(t *testing.T) { + h := New(standard.New(&logger.Config{}), new(config.Config)) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.Base(h.Root)(bitroute.NewControl(w, r)) + }) + + testHandler(t, handler, http.StatusOK, fmt.Sprintf("%s v%s", config.SERVICENAME, version.RELEASE)) +} + +func testHandler(t *testing.T, handler http.HandlerFunc, code int, body string) { + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Error(err) + } + + trw := httptest.NewRecorder() + handler.ServeHTTP(trw, req) + + if trw.Code != code { + t.Error("Expected status code:", code, "got", trw.Code) + } + if trw.Body.String() != body { + t.Error("Expected body", body, "got", trw.Body.String()) + } +} diff --git a/pkg/handlers/health.go b/pkg/handlers/health.go new file mode 100644 index 0000000..659cb51 --- /dev/null +++ b/pkg/handlers/health.go @@ -0,0 +1,13 @@ +package handlers + +import ( + "net/http" + + "github.com/takama/k8sapp/pkg/router" +) + +// Health returns "OK" if service is alive +func (h *Handler) Health(c router.Control) { + c.Code(http.StatusOK) + c.Write(http.StatusText(http.StatusOK)) +} diff --git a/pkg/handlers/health_test.go b/pkg/handlers/health_test.go new file mode 100644 index 0000000..e7ad060 --- /dev/null +++ b/pkg/handlers/health_test.go @@ -0,0 +1,20 @@ +package handlers + +import ( + "net/http" + "testing" + + "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" + "github.com/takama/k8sapp/pkg/logger/standard" + "github.com/takama/k8sapp/pkg/router/bitroute" +) + +func TestHealth(t *testing.T) { + h := New(standard.New(&logger.Config{}), new(config.Config)) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.Base(h.Health)(bitroute.NewControl(w, r)) + }) + + testHandler(t, handler, http.StatusOK, http.StatusText(http.StatusOK)) +} diff --git a/pkg/handlers/ready.go b/pkg/handlers/ready.go new file mode 100644 index 0000000..c3f5a00 --- /dev/null +++ b/pkg/handlers/ready.go @@ -0,0 +1,16 @@ +package handlers + +import ( + "net/http" + + "github.com/takama/k8sapp/pkg/router" +) + +// Ready returns "OK" if service is ready to serve traffic +func (h *Handler) Ready(c router.Control) { + // TODO: possible use cases: + // load data from a database, a message broker, any external services, etc + + c.Code(http.StatusOK) + c.Write(http.StatusText(http.StatusOK)) +} diff --git a/pkg/handlers/ready_test.go b/pkg/handlers/ready_test.go new file mode 100644 index 0000000..9089342 --- /dev/null +++ b/pkg/handlers/ready_test.go @@ -0,0 +1,20 @@ +package handlers + +import ( + "net/http" + "testing" + + "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" + "github.com/takama/k8sapp/pkg/logger/standard" + "github.com/takama/k8sapp/pkg/router/bitroute" +) + +func TestReady(t *testing.T) { + h := New(standard.New(&logger.Config{}), new(config.Config)) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.Base(h.Ready)(bitroute.NewControl(w, r)) + }) + + testHandler(t, handler, http.StatusOK, http.StatusText(http.StatusOK)) +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 8355646..4749092 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -80,11 +80,3 @@ type Config struct { // Use UTC time UTC bool } - -// New returns new logger -func New(cfg *Config) Logger { - // There should be any implementation which compatible with logger interface - // return newLogrus(cfg) - // return newXLog(cfg) - return newStdLog(cfg) -} diff --git a/pkg/logger/logrus.go b/pkg/logger/logrus/logrus.go similarity index 56% rename from pkg/logger/logrus.go rename to pkg/logger/logrus/logrus.go index 426bd17..adffdbc 100644 --- a/pkg/logger/logrus.go +++ b/pkg/logger/logrus/logrus.go @@ -2,29 +2,32 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package logger +package logrus -import "github.com/sirupsen/logrus" +import ( + "github.com/sirupsen/logrus" + "github.com/takama/k8sapp/pkg/logger" +) -// newLogrus creates "github.com/sirupsen/logrus" logger -func newLogrus(config *Config) Logger { +// New creates "github.com/sirupsen/logrus" logger +func New(config *logger.Config) logger.Logger { logger := logrus.New() logger.Level = logrusLevelConverter(config.Level) logger.WithFields(logrus.Fields(config.Fields)) return logger } -func logrusLevelConverter(level Level) logrus.Level { +func logrusLevelConverter(level logger.Level) logrus.Level { switch level { - case LevelDebug: + case logger.LevelDebug: return logrus.DebugLevel - case LevelInfo: + case logger.LevelInfo: return logrus.InfoLevel - case LevelWarn: + case logger.LevelWarn: return logrus.WarnLevel - case LevelError: + case logger.LevelError: return logrus.ErrorLevel - case LevelFatal: + case logger.LevelFatal: return logrus.FatalLevel default: return logrus.InfoLevel diff --git a/pkg/logger/logrus_test.go b/pkg/logger/logrus/logrus_test.go similarity index 60% rename from pkg/logger/logrus_test.go rename to pkg/logger/logrus/logrus_test.go index 4bdd5ae..009ad1c 100644 --- a/pkg/logger/logrus_test.go +++ b/pkg/logger/logrus/logrus_test.go @@ -1,13 +1,24 @@ -package logger +package logrus import ( "testing" "github.com/sirupsen/logrus" + "github.com/takama/k8sapp/pkg/logger" +) + +const ( + customLevel logger.Level = 17 ) func TestLogrusLevel(t *testing.T) { - for _, l := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { + for _, l := range []logger.Level{ + logger.LevelDebug, + logger.LevelInfo, + logger.LevelWarn, + logger.LevelError, + logger.LevelFatal, + } { if logrusLevelConverter(l) == 0 { t.Errorf("Got empty data for %s log level", l.String()) } @@ -19,8 +30,8 @@ func TestLogrusLevel(t *testing.T) { } func TestNewLogrus(t *testing.T) { - log := newLogrus(&Config{ - Level: LevelDebug, + log := New(&logger.Config{ + Level: logger.LevelDebug, }) if log == nil { t.Error("Got uninitialized logrus logger") diff --git a/pkg/logger/standard.go b/pkg/logger/standard/standard.go similarity index 75% rename from pkg/logger/standard.go rename to pkg/logger/standard/standard.go index 54ef8af..52e0675 100644 --- a/pkg/logger/standard.go +++ b/pkg/logger/standard/standard.go @@ -2,20 +2,21 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package logger +package standard import ( "log" "os" "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" ) // UTC contains default UTC suffix const UTC = "+0000 UTC " -// newStdLog returns logger that is compatible with the Logger interface -func newStdLog(cfg *Config) Logger { +// New returns logger that is compatible with the Logger interface +func New(cfg *logger.Config) logger.Logger { var flags int prefix := "[" + config.SERVICENAME + ":" + cfg.Level.String() + "] " if cfg.Out == nil { @@ -42,7 +43,7 @@ func newStdLog(cfg *Config) Logger { // stdLogger implements the Logger interface // except of using logger.Fields type stdLogger struct { - Level + logger.Level Time bool UTC bool stdlog *log.Logger @@ -51,80 +52,80 @@ type stdLogger struct { // Debug logs a debug message func (l *stdLogger) Debug(v ...interface{}) { - if l.Level == LevelDebug { - l.setStdPrefix(LevelDebug) + if l.Level == logger.LevelDebug { + l.setStdPrefix(logger.LevelDebug) l.printStd(v...) } } // Debug logs a debug message with format func (l *stdLogger) Debugf(format string, v ...interface{}) { - if l.Level == LevelDebug { - l.setStdPrefix(LevelDebug) + if l.Level == logger.LevelDebug { + l.setStdPrefix(logger.LevelDebug) l.printfStd(format, v...) } } // Info logs a info message func (l *stdLogger) Info(v ...interface{}) { - if l.Level <= LevelInfo { - l.setStdPrefix(LevelInfo) + if l.Level <= logger.LevelInfo { + l.setStdPrefix(logger.LevelInfo) l.printStd(v...) } } // Info logs a info message with format func (l *stdLogger) Infof(format string, v ...interface{}) { - if l.Level <= LevelInfo { - l.setStdPrefix(LevelInfo) + if l.Level <= logger.LevelInfo { + l.setStdPrefix(logger.LevelInfo) l.printfStd(format, v...) } } // Warn logs a warning message. func (l *stdLogger) Warn(v ...interface{}) { - if l.Level <= LevelWarn { - l.setStdPrefix(LevelWarn) + if l.Level <= logger.LevelWarn { + l.setStdPrefix(logger.LevelWarn) l.printStd(v...) } } // Warn logs a warning message with format. func (l *stdLogger) Warnf(format string, v ...interface{}) { - if l.Level <= LevelWarn { - l.setStdPrefix(LevelWarn) + if l.Level <= logger.LevelWarn { + l.setStdPrefix(logger.LevelWarn) l.printfStd(format, v...) } } // Error logs an error message func (l *stdLogger) Error(v ...interface{}) { - if l.Level <= LevelError { - l.setErrPrefix(LevelError) + if l.Level <= logger.LevelError { + l.setErrPrefix(logger.LevelError) l.printErr(v...) } } // Error logs an error message with format func (l *stdLogger) Errorf(format string, v ...interface{}) { - if l.Level <= LevelError { - l.setErrPrefix(LevelError) + if l.Level <= logger.LevelError { + l.setErrPrefix(logger.LevelError) l.printfErr(format, v...) } } // Fatal logs an error message followed by a call to os.Exit(1) func (l *stdLogger) Fatal(v ...interface{}) { - if l.Level <= LevelFatal { - l.setErrPrefix(LevelFatal) + if l.Level <= logger.LevelFatal { + l.setErrPrefix(logger.LevelFatal) l.printErr(v...) } } // Fatalf logs an error message with format followed by a call to ox.Exit(1) func (l *stdLogger) Fatalf(format string, v ...interface{}) { - if l.Level <= LevelFatal { - l.setErrPrefix(LevelFatal) + if l.Level <= logger.LevelFatal { + l.setErrPrefix(logger.LevelFatal) l.printfErr(format, v...) } } @@ -161,10 +162,10 @@ func (l *stdLogger) printfErr(format string, v ...interface{}) { } } -func (l *stdLogger) setStdPrefix(level Level) { +func (l *stdLogger) setStdPrefix(level logger.Level) { l.stdlog.SetPrefix("[" + config.SERVICENAME + ":" + level.String() + "] ") } -func (l *stdLogger) setErrPrefix(level Level) { +func (l *stdLogger) setErrPrefix(level logger.Level) { l.errlog.SetPrefix("[" + config.SERVICENAME + ":" + level.String() + "] ") } diff --git a/pkg/logger/standard_test.go b/pkg/logger/standard/standard_test.go similarity index 63% rename from pkg/logger/standard_test.go rename to pkg/logger/standard/standard_test.go index 6b151ba..0ff87d0 100644 --- a/pkg/logger/standard_test.go +++ b/pkg/logger/standard/standard_test.go @@ -1,4 +1,4 @@ -package logger +package standard import ( "bytes" @@ -6,13 +6,14 @@ import ( "testing" "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" ) func TestNewLog(t *testing.T) { - config := &Config{} + config := &logger.Config{} New(config) - if config.Level != LevelDebug { - t.Errorf("Invalid level, got %s, want %s", config.Level, LevelDebug) + if config.Level != logger.LevelDebug { + t.Errorf("Invalid level, got %s, want %s", config.Level, logger.LevelDebug) } if config.Out == nil { t.Error("Invalid logger output, got nil, want os.Stdout") @@ -22,51 +23,51 @@ func TestNewLog(t *testing.T) { } } -func logMessage(level Level, message string, out, err *bytes.Buffer, time, utc bool) { - log := New(&Config{ - Level: LevelDebug, +func logMessage(level logger.Level, message string, out, err *bytes.Buffer, time, utc bool) { + log := New(&logger.Config{ + Level: logger.LevelDebug, Out: out, Err: err, Time: time, UTC: utc, }) switch level { - case LevelDebug: + case logger.LevelDebug: log.Debug(message) - case LevelInfo: + case logger.LevelInfo: log.Info(message) - case LevelWarn: + case logger.LevelWarn: log.Warn(message) - case LevelError: + case logger.LevelError: log.Error(message) - case LevelFatal: + case logger.LevelFatal: log.Fatal(message) } } -func logMessageFormated(level Level, format, message string, out, err *bytes.Buffer, time, utc bool) { - log := New(&Config{ - Level: LevelDebug, +func logMessageFormated(level logger.Level, format, message string, out, err *bytes.Buffer, time, utc bool) { + log := New(&logger.Config{ + Level: logger.LevelDebug, Out: out, Err: err, Time: time, UTC: utc, }) switch level { - case LevelDebug: + case logger.LevelDebug: log.Debugf(format, message) - case LevelInfo: + case logger.LevelInfo: log.Infof(format, message) - case LevelWarn: + case logger.LevelWarn: log.Warnf(format, message) - case LevelError: + case logger.LevelError: log.Errorf(format, message) - case LevelFatal: + case logger.LevelFatal: log.Fatalf(format, message) } } -func testOutput(t *testing.T, level Level, message string, formated bool) { +func testOutput(t *testing.T, level logger.Level, message string, formated bool) { var want string prefix := "[" + config.SERVICENAME + ":" + level.String() + "] " out := &bytes.Buffer{} @@ -79,7 +80,7 @@ func testOutput(t *testing.T, level Level, message string, formated bool) { format := "message=%s" logMessageFormated(level, format, message, out, err, false, false) } - if level == LevelDebug || level == LevelInfo || level == LevelWarn { + if level == logger.LevelDebug || level == logger.LevelInfo || level == logger.LevelWarn { if got := out.String(); got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } @@ -91,88 +92,106 @@ func testOutput(t *testing.T, level Level, message string, formated bool) { } func TestLog(t *testing.T) { - for _, level := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { + for _, level := range []logger.Level{ + logger.LevelDebug, + logger.LevelInfo, + logger.LevelWarn, + logger.LevelError, + logger.LevelFatal, + } { testOutput(t, level, level.String()+" message", false) testOutput(t, level, level.String()+" message", true) } } -func checkEmptyMessage(t *testing.T, out *bytes.Buffer, messageLevel, outputlevel Level) { +func checkEmptyMessage(t *testing.T, out *bytes.Buffer, messageLevel, outputlevel logger.Level) { if out.String() == "" { t.Errorf("Got empty %s message for %s output level", messageLevel, outputlevel) } } -func checkNonEmptyMessage(t *testing.T, out *bytes.Buffer, messageLevel, outputlevel Level) { +func checkNonEmptyMessage(t *testing.T, out *bytes.Buffer, messageLevel, outputlevel logger.Level) { if out.String() != "" { t.Errorf("Got non-empty %s message for %s output level", messageLevel, outputlevel) } } -func testLevel(t *testing.T, level, messageLevel Level) { +func testLevel(t *testing.T, level, messageLevel logger.Level) { out := &bytes.Buffer{} err := &bytes.Buffer{} - log := New(&Config{ + log := New(&logger.Config{ Level: level, Out: out, Err: err, }) message := "message" switch messageLevel { - case LevelDebug: + case logger.LevelDebug: log.Debug(message) switch level { - case LevelDebug: + case logger.LevelDebug: checkEmptyMessage(t, out, messageLevel, level) default: checkNonEmptyMessage(t, out, messageLevel, level) } - case LevelInfo: + case logger.LevelInfo: log.Info(message) switch level { - case LevelDebug, LevelInfo: + case logger.LevelDebug, logger.LevelInfo: checkEmptyMessage(t, out, messageLevel, level) default: checkNonEmptyMessage(t, out, messageLevel, level) } - case LevelWarn: + case logger.LevelWarn: log.Warn(message) switch level { - case LevelDebug, LevelInfo, LevelWarn: + case logger.LevelDebug, logger.LevelInfo, logger.LevelWarn: checkEmptyMessage(t, out, messageLevel, level) default: checkNonEmptyMessage(t, out, messageLevel, level) } - case LevelError: + case logger.LevelError: log.Error(message) switch level { - case LevelDebug, LevelInfo, LevelWarn, LevelError: + case logger.LevelDebug, logger.LevelInfo, logger.LevelWarn, logger.LevelError: checkEmptyMessage(t, err, messageLevel, level) default: checkNonEmptyMessage(t, err, messageLevel, level) } - case LevelFatal: + case logger.LevelFatal: log.Fatal(message) checkEmptyMessage(t, err, messageLevel, level) } } func TestLevel(t *testing.T) { - for _, level := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { - for _, messageLevel := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { + for _, level := range []logger.Level{ + logger.LevelDebug, + logger.LevelInfo, + logger.LevelWarn, + logger.LevelError, + logger.LevelFatal, + } { + for _, messageLevel := range []logger.Level{ + logger.LevelDebug, + logger.LevelInfo, + logger.LevelWarn, + logger.LevelError, + logger.LevelFatal, + } { testLevel(t, level, messageLevel) } } } -func testOutputWithTime(t *testing.T, level Level, message string) { +func testOutputWithTime(t *testing.T, level logger.Level, message string) { prefix := "[" + config.SERVICENAME + ":" + level.String() + "] " want := prefix + "__TIME__ " + UTC + message + "\n" out := &bytes.Buffer{} err := &bytes.Buffer{} logMessage(level, message, out, err, true, true) - if level == LevelDebug || level == LevelInfo || level == LevelWarn { + if level == logger.LevelDebug || level == logger.LevelInfo || level == logger.LevelWarn { if got := out.String(); !strings.Contains(got, UTC) || !strings.Contains(got, prefix) || !strings.Contains(got, message) { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) @@ -185,13 +204,13 @@ func testOutputWithTime(t *testing.T, level Level, message string) { } } -func testOutputFormatedWithTime(t *testing.T, level Level, message string) { +func testOutputFormatedWithTime(t *testing.T, level logger.Level, message string) { prefix := "[" + config.SERVICENAME + ":" + level.String() + "] " want := prefix + "__TIME__ " + UTC + message + "\n" out := &bytes.Buffer{} err := &bytes.Buffer{} logMessageFormated(level, "%s", message, out, err, true, true) - if level == LevelDebug || level == LevelInfo || level == LevelWarn { + if level == logger.LevelDebug || level == logger.LevelInfo || level == logger.LevelWarn { if got := out.String(); !strings.Contains(got, UTC) || !strings.Contains(got, prefix) || !strings.Contains(got, message) { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) @@ -205,7 +224,13 @@ func testOutputFormatedWithTime(t *testing.T, level Level, message string) { } func TestLogWithTime(t *testing.T) { - for _, level := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { + for _, level := range []logger.Level{ + logger.LevelDebug, + logger.LevelInfo, + logger.LevelWarn, + logger.LevelError, + logger.LevelFatal, + } { testOutputWithTime(t, level, level.String()+" message") testOutputFormatedWithTime(t, level, level.String()+" message") } diff --git a/pkg/logger/xlog.go b/pkg/logger/xlog/xlog.go similarity index 86% rename from pkg/logger/xlog.go rename to pkg/logger/xlog/xlog.go index 76ec3f4..7df3020 100644 --- a/pkg/logger/xlog.go +++ b/pkg/logger/xlog/xlog.go @@ -8,10 +8,11 @@ import ( "os" "github.com/rs/xlog" + "github.com/takama/k8sapp/pkg/logger" ) // newXLog creates "github.com/rs/xlog" logger -func newXLog(config *Config) Logger { +func newXLog(config *logger.Config) logger.Logger { var out xlog.Output switch config.Err { // We should find more matches between types of output diff --git a/pkg/logger/xlog_test.go b/pkg/logger/xlog/xlog_test.go similarity index 61% rename from pkg/logger/xlog_test.go rename to pkg/logger/xlog/xlog_test.go index 2d43c97..fac37c5 100644 --- a/pkg/logger/xlog_test.go +++ b/pkg/logger/xlog/xlog_test.go @@ -3,17 +3,19 @@ package logger import ( "os" "testing" + + "github.com/takama/k8sapp/pkg/logger" ) func TestNewXLog(t *testing.T) { - log1 := newXLog(&Config{ - Level: LevelDebug, + log1 := newXLog(&logger.Config{ + Level: logger.LevelDebug, }) if log1 == nil { t.Error("Got uninitialized XLog logger") } - log2 := newXLog(&Config{ - Level: LevelInfo, + log2 := newXLog(&logger.Config{ + Level: logger.LevelInfo, Out: os.Stdout, Err: os.Stdout, }) diff --git a/pkg/router/bitroute.go b/pkg/router/bitroute.go new file mode 100644 index 0000000..037d4b3 --- /dev/null +++ b/pkg/router/bitroute.go @@ -0,0 +1,83 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package router + +import "net/http" + +// Control interface contains methods that control +// HTTP header, URL/post query parameters, request/response +// and HTTP output like Code(), Write(), etc. +type Control interface { + // Request returns *http.Request + Request() *http.Request + + // Query searches URL/Post query parameters by key. + // If there are no values associated with the key, an empty string is returned. + Query(key string) string + + // Param sets URL/Post key/value query parameters. + Param(key, value string) + + // Response writer section + + // Header represents http.ResponseWriter header, the key-value pairs in an HTTP header. + Header() http.Header + + // Code sets HTTP status code e.g. http.StatusOk + Code(code int) + + // Write prepared header, status code and body data into http output. + Write(data interface{}) + + // TODO Add more control methods. +} + +// BitRoute interface contains base http methods e.g. GET, PUT, POST +// and defines your own handlers that is useful in some use cases +type BitRoute interface { + // Standard methods + + // GET registers a new request handle for HTTP GET method. + GET(path string, f func(Control)) + // PUT registers a new request handle for HTTP PUT method. + PUT(path string, f func(Control)) + // POST registers a new request handle for HTTP POST method. + POST(path string, f func(Control)) + // DELETE registers a new request handle for HTTP DELETE method. + DELETE(path string, f func(Control)) + // HEAD registers a new request handle for HTTP HEAD method. + HEAD(path string, f func(Control)) + // OPTIONS registers a new request handle for HTTP OPTIONS method. + OPTIONS(path string, f func(Control)) + // PATCH registers a new request handle for HTTP PATCH method. + PATCH(path string, f func(Control)) + + // User defined options and handlers + + // If enabled, the router automatically replies to OPTIONS requests. + // Nevertheless OPTIONS handlers take priority over automatic replies. + // By default this option is disabled + UseOptionsReplies(bool) + + // SetupNotAllowedHandler defines own handler which is called when a request + // cannot be routed. + SetupNotAllowedHandler(func(Control)) + + // SetupNotFoundHandler allows to define own handler for undefined URL path. + // If it is not set, http.NotFound is used. + SetupNotFoundHandler(func(Control)) + + // SetupRecoveryHandler allows to define handler that called when panic happen. + // The handler prevents your server from crashing and should be used to return + // http status code http.StatusInternalServerError (500) + SetupRecoveryHandler(func(Control)) + + // SetupMiddleware defines handler that is allowed to take control + // before it is called standard methods above e.g. GET, PUT. + SetupMiddleware(func(func(Control)) func(Control)) + + // Listen and serve on requested host and port e.g "0.0.0.0:8080" + Listen(hostPort string) error +} diff --git a/pkg/router/bitroute/control.go b/pkg/router/bitroute/control.go new file mode 100644 index 0000000..4d3f9d1 --- /dev/null +++ b/pkg/router/bitroute/control.go @@ -0,0 +1,101 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package bitroute + +import ( + "compress/gzip" + "encoding/json" + "net/http" + "strings" + + "github.com/takama/k8sapp/pkg/router" +) + +type control struct { + req *http.Request + w http.ResponseWriter + code int + params []struct { + key string + value string + } +} + +// NewControl returns new control that implement Control interface. +func NewControl(w http.ResponseWriter, req *http.Request) router.Control { + return &control{ + req: req, + w: w, + } +} + +// Request returns *http.Request +func (c *control) Request() *http.Request { + return c.req +} + +// Query searches URL/Post value by key. +// If there are no values associated with the key, an empty string is returned. +func (c *control) Query(key string) string { + for idx := range c.params { + if c.params[idx].key == key { + return c.params[idx].value + } + } + + return c.req.URL.Query().Get(key) +} + +// Param sets URL/Post key/value params. +func (c *control) Param(key, value string) { + c.params = append(c.params, struct{ key, value string }{key: key, value: value}) +} + +// Response writer section +// Header represents http.ResponseWriter header, the key-value pairs in an HTTP header. +func (c *control) Header() http.Header { + return c.w.Header() +} + +// Code sets HTTP status code e.g. http.StatusOk +func (c *control) Code(code int) { + if code >= 100 && code < 600 { + c.code = code + } +} + +// Write writes data into http output. +func (c *control) Write(data interface{}) { + var content []byte + + if str, ok := data.(string); ok { + content = []byte(str) + } else { + var err error + content, err = json.Marshal(data) + if err != nil { + c.w.WriteHeader(http.StatusInternalServerError) + c.w.Write([]byte(err.Error())) + return + } + if c.w.Header().Get("Content-type") == "" { + c.w.Header().Add("Content-type", "application/json") + } + } + if strings.Contains(c.req.Header.Get("Accept-Encoding"), "gzip") { + c.w.Header().Add("Content-Encoding", "gzip") + if c.code > 0 { + c.w.WriteHeader(c.code) + } + gz := gzip.NewWriter(c.w) + gz.Write(content) + gz.Close() + } else { + if c.code > 0 { + c.w.WriteHeader(c.code) + } + c.w.Write(content) + } +} diff --git a/pkg/router/bitroute/control_test.go b/pkg/router/bitroute/control_test.go new file mode 100644 index 0000000..dc9a2da --- /dev/null +++ b/pkg/router/bitroute/control_test.go @@ -0,0 +1,125 @@ +package bitroute + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +type prm struct { + Key, Value string +} + +var params = []prm{ + {"name", "John"}, + {"age", "32"}, + {"gender", "M"}, +} + +var testParamsData = `[{"Key":"name","Value":"John"},{"Key":"age","Value":"32"},{"Key":"gender","Value":"M"}]` +var testParamGzipData = []byte{ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 138, 174, 86, 242, 78, 173, 84, 178, 82, 202, + 75, 204, 77, 85, 210, 81, 10, 75, 204, 41, 77, 85, 178, 82, 242, 202, 207, 200, + 83, 170, 213, 129, 201, 38, 166, 35, 75, 26, 27, 33, 73, 165, 167, 230, 165, 164, + 22, 33, 201, 250, 42, 213, 198, 2, 2, 0, 0, 255, 255, 196, 73, 247, 37, 87, 0, 0, 0, +} + +func TestParamsQueryGet(t *testing.T) { + + c := new(control) + for _, param := range params { + c.Param(param.Key, param.Value) + } + for _, param := range params { + value := c.Query(param.Key) + if value != param.Value { + t.Error("Expected for", param.Key, ":", param.Value, ", got", value) + } + } +} + +func TestWriterHeader(t *testing.T) { + req, err := http.NewRequest("GET", "hello/:name", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + c := NewControl(trw, req) + request := c.Request() + if request != req { + t.Error("Expected", req.URL.String(), "got", request.URL.String()) + } + trw.Header().Add("Test", "TestValue") + c = NewControl(trw, req) + expected := trw.Header().Get("Test") + value := c.Header().Get("Test") + if value != expected { + t.Error("Expected", expected, "got", value) + } +} + +func TestWriterCode(t *testing.T) { + c := new(control) + // code transcends, must be less than 600 + c.Code(777) + if c.code != 0 { + t.Error("Expected code", "0", "got", c.code) + } + c.Code(404) + if c.code != 404 { + t.Error("Expected code", "404", "got", c.code) + } +} + +func TestWrite(t *testing.T) { + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + c := NewControl(trw, req) + c.Write("Hello") + if trw.Body.String() != "Hello" { + t.Error("Expected", "Hello", "got", trw.Body.String()) + } + contentType := trw.Header().Get("Content-type") + expected := "text/plain; charset=utf-8" + if contentType != expected { + t.Error("Expected", expected, "got", contentType) + } + trw = httptest.NewRecorder() + c = NewControl(trw, req) + c.Code(http.StatusOK) + c.Write(params) + if trw.Body.String() != testParamsData { + t.Error("Expected", testParamsData, "got", trw.Body.String()) + } + contentType = trw.Header().Get("Content-type") + expected = "application/json" + if contentType != expected { + t.Error("Expected", expected, "got", contentType) + } + req.Header.Add("Accept-Encoding", "gzip, deflate") + trw = httptest.NewRecorder() + c = NewControl(trw, req) + c.Code(http.StatusAccepted) + c.Write(params) + if trw.Body.String() != string(testParamGzipData) { + t.Error("Expected", testParamGzipData, "got", trw.Body.String()) + } + contentEncoding := trw.Header().Get("Content-Encoding") + expected = "gzip" + if contentEncoding != expected { + t.Error("Expected", expected, "got", contentEncoding) + } + trw = httptest.NewRecorder() + c = NewControl(trw, req) + c.Write(func() {}) + if trw.Code != http.StatusInternalServerError { + t.Error("Expected", http.StatusInternalServerError, "got", trw.Code) + } + expected = "application/json" + if contentType != expected { + t.Error("Expected", expected, "got", contentType) + } +} diff --git a/pkg/router/bitroute/parser.go b/pkg/router/bitroute/parser.go new file mode 100644 index 0000000..1029091 --- /dev/null +++ b/pkg/router/bitroute/parser.go @@ -0,0 +1,236 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package bitroute + +import ( + "sort" + + "github.com/takama/k8sapp/pkg/router" +) + +const ( + maxLevel = 255 + asterisk = "*" +) + +type handle func(router.Control) + +type parser struct { + fields map[uint8]records + static map[string]handle + wildcard records +} + +type record struct { + key uint16 + handle handle + parts []string +} + +type param struct { + key string + value string +} +type records []*record + +func (n records) Len() int { return len(n) } +func (n records) Swap(i, j int) { n[i], n[j] = n[j], n[i] } +func (n records) Less(i, j int) bool { return n[i].key < n[j].key } + +func newParser() *parser { + return &parser{ + fields: make(map[uint8]records), + static: make(map[string]handle), + wildcard: records{}, + } +} + +func (p *parser) register(path string, h handle) bool { + if trim(path, " ") == asterisk { + p.static[asterisk] = h + + return true + } + if parts, ok := split(path); ok { + var static, dynamic, wildcard uint16 + for _, value := range parts { + if len(value) >= 1 && value[0:1] == ":" { + dynamic++ + } else if len(value) == 1 && value == "*" { + wildcard++ + } else { + static++ + } + } + if wildcard > 0 { + p.wildcard = append(p.wildcard, &record{key: dynamic<<8 + static, handle: h, parts: parts}) + } else if dynamic == 0 { + p.static["/"+join(parts)] = h + } else { + level := uint8(len(parts)) + p.fields[level] = append(p.fields[level], &record{key: dynamic<<8 + static, handle: h, parts: parts}) + sort.Sort(records(p.fields[level])) + } + return true + } + + return false +} + +func (p *parser) get(path string) (h handle, result []param, ok bool) { + if h, ok := p.static[asterisk]; ok { + return h, nil, true + } + if h, ok := p.static[path]; ok { + return h, nil, true + } + if parts, ok := split(path); ok { + if h, ok := p.static["/"+join(parts)]; ok { + return h, nil, true + } + if data := p.fields[uint8(len(parts))]; data != nil { + if h, result, ok := parseParams(data, parts); ok { + return h, result, ok + } + } + // try to match wildcard route + if h, result, ok := parseParams(p.wildcard, parts); ok { + return h, result, ok + } + } + + return nil, nil, false +} + +func split(path string) ([]string, bool) { + sdata := explode(trim(path, "/")) + if len(sdata) == 0 { + return sdata, true + } + var result []string + ind := 0 + if len(sdata) < maxLevel { + result = make([]string, len(sdata)) + for _, value := range sdata { + if v := trim(value, " "); v == "" { + continue + } else { + result[ind] = v + ind++ + } + } + return result[0:ind], true + } + + return nil, false +} + +func trim(str, sep string) string { + result := str + for { + if len(result) >= 1 && result[0:1] == sep { + result = result[1:] + } else { + break + } + } + for { + if len(result) >= 1 && result[len(result)-1:] == sep { + result = result[:len(result)-1] + } else { + break + } + } + return result +} + +func join(parts []string) string { + if len(parts) == 0 { + return "" + } + if len(parts) == 1 { + return parts[0] + } + n := len(parts) - 1 + for i := 0; i < len(parts); i++ { + n += len(parts[i]) + } + + b := make([]byte, n) + bp := copy(b, parts[0]) + for _, s := range parts[1:] { + bp += copy(b[bp:], "/") + bp += copy(b[bp:], s) + } + return string(b) +} + +func explode(s string) []string { + if len(s) == 0 { + return []string{} + } + n := 1 + sep := "/" + c := sep[0] + for i := 0; i < len(s); i++ { + if s[i] == c { + n++ + } + } + start := 0 + a := make([]string, n) + na := 0 + for i := 0; i+1 <= len(s) && na+1 < n; i++ { + if s[i] == c { + a[na] = s[start:i] + na++ + start = i + 1 + } + } + a[na] = s[start:] + return a[0 : na+1] +} + +func parseParams(data records, parts []string) (h handle, result []param, ok bool) { + for _, nds := range data { + values := nds.parts + result = nil + found := true + for idx, value := range values { + if len(value) == 1 && value == "*" { + break + } else if value != parts[idx] && !(len(value) >= 1 && value[0:1] == ":") { + found = false + break + } else { + if len(value) >= 1 && value[0:1] == ":" { + result = append(result, param{key: value, value: parts[idx]}) + } + } + } + if found { + return nds.handle, result, true + } + } + + return nil, nil, false +} + +func (p *parser) routes() []string { + var rs []string + for path := range p.static { + rs = append(rs, path) + } + for _, records := range p.fields { + for _, record := range records { + rs = append(rs, "/"+join(record.parts)) + } + } + for _, record := range p.wildcard { + rs = append(rs, "/"+join(record.parts)) + } + + return rs +} diff --git a/pkg/router/bitroute/parser_test.go b/pkg/router/bitroute/parser_test.go new file mode 100644 index 0000000..67912ad --- /dev/null +++ b/pkg/router/bitroute/parser_test.go @@ -0,0 +1,331 @@ +package bitroute + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/takama/k8sapp/pkg/router" +) + +type registered struct { + path string + h handle +} + +type expected struct { + request string + data string + paramCount int + params []param +} + +var setOfRegistered = []registered{ + { + "/hello/John", + func(c router.Control) { + c.Write("Hello from static path") + }, + }, + { + "/hello/:name", + func(c router.Control) { + c.Write("Hello " + c.Query(":name")) + }, + }, + { + "/:h/:n", + func(c router.Control) { + c.Write(c.Query(":n") + " from " + c.Query(":h")) + }, + }, + { + "/products/book/orders/:id", + func(c router.Control) { + c.Write("Product: book order# " + c.Query(":id")) + }, + }, + { + "/products/:name/orders/:id", + func(c router.Control) { + c.Write("Product: " + c.Query(":name") + " order# " + c.Query(":id")) + }, + }, + { + "/products/:name/:order/:id", + func(c router.Control) { + c.Write("Product: " + c.Query(":name") + " # " + c.Query(":id")) + }, + }, + { + "/:product/:name/:order/:id", + func(c router.Control) { + c.Write(c.Query(":product") + " " + c.Query(":name") + " " + c.Query(":order") + " # " + c.Query(":id")) + }, + }, + { + "/static/*", + func(c router.Control) { + c.Write("Hello from star static path") + }, + }, + { + "/files/:dir/*", + func(c router.Control) { + c.Write(c.Query(":dir")) + }, + }, +} + +var setOfExpected = []expected{ + { + "/hello/John", + "Hello from static path", + 0, + []param{}, + }, + { + "/hello/Jane", + "Hello Jane", + 1, + []param{ + {":name", "Jane"}, + }, + }, + { + "/hell/jack", + "jack from hell", + 2, + []param{ + {":h", "hell"}, + {":n", "jack"}, + }, + }, + { + "/products/book/orders/12", + "Product: book order# 12", + 1, + []param{ + {":id", "12"}, + }, + }, + { + "/products/table/orders/23", + "Product: table order# 23", + 2, + []param{ + {":name", "table"}, + {":id", "23"}, + }, + }, + { + "/products/pen/orders/11", + "Product: pen order# 11", + 2, + []param{ + {":name", "pen"}, + {":id", "11"}, + }, + }, + { + "/products/pen/order/10", + "Product: pen # 10", + 3, + []param{ + {":name", "pen"}, + {":order", "order"}, + {":id", "10"}, + }, + }, + { + "/product/pen/order/10", + "product pen order # 10", + 4, + []param{ + {":product", "product"}, + {":name", "pen"}, + {":order", "order"}, + {":id", "10"}, + }, + }, + { + "/static/greetings/something", + "Hello from star static path", + 0, + []param{}, + }, + { + "/files/css/style.css", + "css", + 1, + []param{ + {":dir", "css"}, + }, + }, + { + "/files/js/app.js", + "js", + 1, + []param{ + {":dir", "js"}, + }, + }, +} + +func TestParserRegisterGet(t *testing.T) { + p := newParser() + for _, request := range setOfRegistered { + p.register(request.path, request.h) + } + for _, exp := range setOfExpected { + h, params, ok := p.get(exp.request) + if !ok { + t.Error("Error: get data for path", exp.request) + } + if len(params) != exp.paramCount { + t.Error("Expected length of param", exp.paramCount, "got", len(params)) + } + trw := httptest.NewRecorder() + req, err := http.NewRequest("GET", "", nil) + if err != nil { + t.Error("Error creating new request") + } + c := NewControl(trw, req) + for _, item := range params { + c.Param(item.key, item.value) + } + h(c) + if trw.Body.String() != exp.data { + t.Error("Expected", exp.data, "got", trw.Body.String()) + } + } +} + +func TestParserSplit(t *testing.T) { + path := []string{ + "/api/v1/module", + "/api//v1/module/", + "/module///name//", + "module//:name", + "/:param1/:param2/", + strings.Repeat("/A", 300), + } + expected := [][]string{ + {"api", "v1", "module"}, + {"api", "v1", "module"}, + {"module", "name"}, + {"module", ":name"}, + {":param1", ":param2"}, + } + + if part, ok := split(" "); ok { + if len(part) != 0 { + t.Error("Error: split data for path '/'", part) + } + } else { + t.Error("Error: split data for path '/'") + } + + if part, ok := split("///"); ok { + if len(part) != 0 { + t.Error("Error: split data for path '/'", part) + } + } else { + t.Error("Error: split data for path '/'") + } + + if part, ok := split(" / // "); ok { + if len(part) != 0 { + t.Error("Error: split data for path '/'", part) + } + } else { + t.Error("Error: split data for path '/'") + } + + for idx, p := range path { + parts, ok := split(p) + if !ok { + if strings.HasPrefix(p, "/A/A/A") { + parser := newParser() + result := parser.register(p, func(router.Control) {}) + if result { + t.Error("Expected false result, got", result) + } + continue + } + t.Error("Error: split data for path", p) + } + for i, part := range parts { + if expected[idx][i] != part { + t.Error("Expected", expected[idx][i], "got", part) + } + } + } +} + +func TestGetRoutes(t *testing.T) { + for _, request := range setOfRegistered { + p := newParser() + p.register(request.path, request.h) + routes := p.routes() + if len(routes) != 1 { + t.Error("Expected 1 route, got", len(routes)) + } + if request.path != routes[0] { + t.Error("Expected", request.path, "got", routes[0]) + } + } +} + +func TestRegisterAsterisk(t *testing.T) { + data := "Any path is ok" + p := newParser() + p.register("*", func(c router.Control) { + c.Write(data) + }) + path := "/any/path/is/ok" + h, params, ok := p.get(path) + if !ok { + t.Error("Error: get data for path", path) + } + trw := httptest.NewRecorder() + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Error("Error creating new request") + } + c := NewControl(trw, req) + for _, item := range params { + c.Param(item.key, item.value) + } + h(c) + if trw.Body.String() != data { + t.Error("Expected", data, "got", trw.Body.String()) + } +} + +func TestSortRecords(t *testing.T) { + var r = records{ + { + key: 111, + }, + { + key: 222, + }, + } + if r.Len() != len(r) { + t.Error("Len doesn't work, expected", len(r), "got", r.Len()) + } + first := r[0].key + second := r[1].key + r.Swap(0, 1) + if r[0].key != second { + t.Error("Swap doesn't work, expected", second, "got", r[0].key) + } + if r[1].key != first { + t.Error("Swap doesn't work, expected", first, "got", r[1].key) + } + if r.Less(0, 1) { + t.Error("Less doesn't work, expected", r[1].key, "less then", r[0].key) + } +} diff --git a/pkg/router/bitroute/serve.go b/pkg/router/bitroute/serve.go new file mode 100644 index 0000000..5e3bd1b --- /dev/null +++ b/pkg/router/bitroute/serve.go @@ -0,0 +1,186 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package bitroute + +import ( + "net/http" + "strings" + + "github.com/takama/k8sapp/pkg/router" +) + +type bitroute struct { + // List of handlers that associated with known http methods (GET, POST ...) + handlers map[string]*parser + + // If enabled, the router automatically replies to OPTIONS requests. + // Nevertheless OPTIONS handlers take priority over automatic replies. + optionsRepliesEnabled bool + + // Configurable handler which is called when a request cannot be routed. + notAllowed func(router.Control) + + // Configurable handler which is called when panic happen. + recoveryHandler func(router.Control) + + // Configurable handler which is allowed to take control + // before it is called standard methods e.g. GET, PUT. + middlewareHandler func(func(router.Control)) func(router.Control) + + // Configurable http.Handler which is called when URL path has not defined method. + // If it is not set, http.NotFound is used. + notFound func(router.Control) +} + +// New returns new router that implement Router interface. +func New() router.BitRoute { + return &bitroute{ + handlers: make(map[string]*parser), + } +} + +// GET registers a new request handle for HTTP GET method. +func (r *bitroute) GET(path string, f func(router.Control)) { + r.register("GET", path, f) +} + +// PUT registers a new request handle for HTTP PUT method. +func (r *bitroute) PUT(path string, f func(router.Control)) { + r.register("PUT", path, f) +} + +// POST registers a new request handle for HTTP POST method. +func (r *bitroute) POST(path string, f func(router.Control)) { + r.register("POST", path, f) +} + +// DELETE registers a new request handle for HTTP DELETE method. +func (r *bitroute) DELETE(path string, f func(router.Control)) { + r.register("DELETE", path, f) +} + +// HEAD registers a new request handle for HTTP HEAD method. +func (r *bitroute) HEAD(path string, f func(router.Control)) { + r.register("HEAD", path, f) +} + +// OPTIONS registers a new request handle for HTTP OPTIONS method. +func (r *bitroute) OPTIONS(path string, f func(router.Control)) { + r.register("OPTIONS", path, f) +} + +// PATCH registers a new request handle for HTTP PATCH method. +func (r *bitroute) PATCH(path string, f func(router.Control)) { + r.register("PATCH", path, f) +} + +// If enabled, the router automatically replies to OPTIONS requests. +// Nevertheless OPTIONS handlers take priority over automatic replies. +// By default this option is disabled +func (r *bitroute) UseOptionsReplies(enabled bool) { + r.optionsRepliesEnabled = enabled +} + +// SetupNotAllowedHandler defines own handler which is called when a request +// cannot be routed. +func (r *bitroute) SetupNotAllowedHandler(f func(router.Control)) { + r.notAllowed = f +} + +// SetupNotFoundHandler allows to define own handler for undefined URL path. +// If it is not set, http.NotFound is used. +func (r *bitroute) SetupNotFoundHandler(f func(router.Control)) { + r.notFound = f +} + +// SetupRecoveryHandler allows to define handler that called when panic happen. +// The handler prevents your server from crashing and should be used to return +// http status code http.StatusInternalServerError (500) +func (r *bitroute) SetupRecoveryHandler(f func(router.Control)) { + r.recoveryHandler = f +} + +// SetupMiddleware defines handler is allowed to take control +// before it is called standard methods e.g. GET, PUT. +func (r *bitroute) SetupMiddleware(f func(func(router.Control)) func(router.Control)) { + r.middlewareHandler = f +} + +// Listen and serve on requested host and port +func (r *bitroute) Listen(hostPort string) error { + return http.ListenAndServe(hostPort, r) +} + +// registers a new handler with the given path and method. +func (r *bitroute) register(method, path string, f func(router.Control)) { + if r.handlers[method] == nil { + r.handlers[method] = newParser() + } + r.handlers[method].register(path, f) +} + +func (r *bitroute) recovery(w http.ResponseWriter, req *http.Request) { + if recv := recover(); recv != nil { + c := NewControl(w, req) + r.recoveryHandler(c) + } +} + +// AllowedMethods returns list of allowed methods +func (r *bitroute) allowedMethods(path string) []string { + var allowed []string + for method, parser := range r.handlers { + if _, _, ok := parser.get(path); ok { + allowed = append(allowed, method) + } + } + + return allowed +} + +// ServeHTTP implements http.Handler interface. +func (r *bitroute) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if r.recoveryHandler != nil { + defer r.recovery(w, req) + } + if _, ok := r.handlers[req.Method]; ok { + if handle, params, ok := r.handlers[req.Method].get(req.URL.Path); ok { + c := NewControl(w, req) + if len(params) > 0 { + for _, item := range params { + c.Param(item.key, item.value) + } + } + if r.middlewareHandler != nil { + r.middlewareHandler(handle)(c) + } else { + handle(c) + } + return + } + } + allowed := r.allowedMethods(req.URL.Path) + + if len(allowed) == 0 { + if r.notFound != nil { + c := NewControl(w, req) + r.notFound(c) + } else { + http.NotFound(w, req) + } + return + } + + w.Header().Set("Allow", strings.Join(allowed, ", ")) + if req.Method == "OPTIONS" && r.optionsRepliesEnabled { + return + } + if r.notAllowed != nil { + c := NewControl(w, req) + r.notAllowed(c) + } else { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + } +} diff --git a/pkg/router/bitroute/serve_test.go b/pkg/router/bitroute/serve_test.go new file mode 100644 index 0000000..c476a1c --- /dev/null +++ b/pkg/router/bitroute/serve_test.go @@ -0,0 +1,446 @@ +package bitroute + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/takama/k8sapp/pkg/router" +) + +func getRouterForTesting() *bitroute { + return &bitroute{ + handlers: make(map[string]*parser), + } +} + +func TestNewRouter(t *testing.T) { + r := New() + if r == nil { + t.Error("Expected new router, got nil") + } + err := r.Listen("$") + if err == nil { + t.Error("Expected error if used incorrect host and port") + } +} + +func TestRouterGetRootStatic(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler for root static path + r.GET("/", func(c router.Control) { + c.Write("Root") + }) + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Root" { + t.Error("Expected", "Root", "got", trw.Body.String()) + } +} + +func TestRouterGetStatic(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler for static path + r.GET("/hello", func(c router.Control) { + c.Write("Hello") + }) + req, err := http.NewRequest("GET", "/hello", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Hello" { + t.Error("Expected", "Hello", "got", trw.Body.String()) + } +} + +func TestRouterGetParameter(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler with parameter + r.GET("/hello/:name", func(c router.Control) { + c.Write("Hello " + c.Query(":name")) + }) + req, err := http.NewRequest("GET", "/hello/John", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Hello John" { + t.Error("Expected", "Hello John", "got", trw.Body.String()) + } +} + +func TestRouterGetParameterFromClassicUrl(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler with two parameters + r.GET("/users/:name", func(c router.Control) { + c.Write("Users: " + c.Query(":name") + " " + c.Query("name")) + }) + req, err := http.NewRequest("GET", "/users/Jane/?name=Joe", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Users: Jane Joe" { + t.Error("Expected", "Users: Jane Joe", "got", trw.Body.String()) + } +} + +func TestRouterPostJSONData(t *testing.T) { + r := getRouterForTesting() + // Registers POST handler + r.POST("/users", func(c router.Control) { + body, err := ioutil.ReadAll(c.Request().Body) + if err != nil { + t.Error(err) + } + var values map[string]string + if err := json.Unmarshal(body, &values); err != nil { + t.Error(err) + } + c.Write("User: " + values["name"]) + }) + req, err := http.NewRequest("POST", "/users/", strings.NewReader(`{"name": "Tom"}`)) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "User: Tom" { + t.Error("Expected", "User: Tom", "got", trw.Body.String()) + } +} + +func TestRouterPutJSONData(t *testing.T) { + r := getRouterForTesting() + // Registers PUT handler + r.PUT("/users", func(c router.Control) { + body, err := ioutil.ReadAll(c.Request().Body) + if err != nil { + t.Error(err) + } + var values map[string]string + if err := json.Unmarshal(body, &values); err != nil { + t.Error(err) + } + c.Write("Users: " + values["name1"] + " " + values["name2"]) + }) + req, err := http.NewRequest("PUT", "/users/", strings.NewReader(`{"name1": "user1", "name2": "user2"}`)) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Users: user1 user2" { + t.Error("Expected", "Users: user1 user2", "got", trw.Body.String()) + } +} + +func TestRouterDelete(t *testing.T) { + r := getRouterForTesting() + // Registers DELETE handler + r.DELETE("/users", func(c router.Control) { + c.Write("Users deleted") + }) + req, err := http.NewRequest("DELETE", "/users/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Users deleted" { + t.Error("Expected", "Users deleted", "got", trw.Body.String()) + } +} + +func TestRouterHead(t *testing.T) { + r := getRouterForTesting() + // Registers HEAD handler + r.HEAD("/command", func(c router.Control) { + c.Header().Add("test", "value") + }) + req, err := http.NewRequest("HEAD", "/command/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Header().Get("test") + if result != "value" { + t.Error("Expected value", "got", result) + } +} + +func TestRouterOptions(t *testing.T) { + r := getRouterForTesting() + // Registers OPTIONS handler + r.OPTIONS("/option", func(c router.Control) { + c.Code(http.StatusOK) + }) + req, err := http.NewRequest("OPTIONS", "/option/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusOK { + t.Error("Expected", http.StatusOK, "got", result) + } +} + +func TestRouterPatch(t *testing.T) { + r := getRouterForTesting() + // Registers PATCH handler + r.PATCH("/patch", func(c router.Control) { + c.Code(http.StatusOK) + }) + req, err := http.NewRequest("PATCH", "/patch/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusOK { + t.Error("Expected", http.StatusOK, "got", result) + } +} + +func TestRouterUseOptionsReplies(t *testing.T) { + r := getRouterForTesting() + path := "/options" + r.GET(path, func(c router.Control) { + c.Code(http.StatusOK) + }) + r.UseOptionsReplies(true) + req, err := http.NewRequest("OPTIONS", path, nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + code := trw.Code + if code != http.StatusOK { + t.Error("Expected", http.StatusOK, "got", code) + } + header := trw.Header().Get("Allow") + expected := "GET" + if header != expected { + t.Error("Expected", expected, "got", header) + } +} + +func TestRouterNotFound(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler + r.GET("/found", func(c router.Control) { + c.Code(http.StatusOK) + }) + req, err := http.NewRequest("GET", "/not-found/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusNotFound { + t.Error("Expected", http.StatusNotFound, "got", result) + } +} + +func TestRouterAllowedMethods(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler + path := "/allowed" + r.GET(path, func(c router.Control) { + c.Code(http.StatusOK) + }) + // Registers PUT handler + r.PUT(path, func(c router.Control) { + c.Code(http.StatusAccepted) + }) + result := r.allowedMethods(path) + for _, method := range []string{"GET", "PUT"} { + var exists bool + for _, allowed := range result { + if method == allowed { + exists = true + } + } + if !exists { + t.Error("Allowed method(s) not found in", result) + } + } + for _, method := range []string{"POST", "DELETE", "HEAD", "OPTIONS", "PATCH"} { + var exists bool + for _, allowed := range result { + if method == allowed { + exists = true + } + } + if exists { + t.Error("Not allowed method(s) found in", result) + } + } +} + +func TestRouterNotAllowed(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler + path := "/allowed" + message := http.StatusText(http.StatusMethodNotAllowed) + "\n" + r.GET(path, func(c router.Control) { + c.Code(http.StatusOK) + }) + // Registers PUT handler + r.PUT(path, func(c router.Control) { + c.Code(http.StatusAccepted) + }) + req, err := http.NewRequest("POST", path, nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusMethodNotAllowed { + t.Error("Expected", http.StatusMethodNotAllowed, "got", result) + } + header := trw.Header().Get("Allow") + expected1 := "GET, PUT" + expected2 := "PUT, GET" + if header != expected1 && header != expected2 { + t.Error("Expected", expected1, "or", expected2, "got", header) + } + if trw.Body.String() != message { + t.Error("Expected", message, "got", trw.Body.String()) + } +} + +func TestRouterSetupNotAllowedHandler(t *testing.T) { + r := getRouterForTesting() + message := http.StatusText(http.StatusForbidden) + path := "/not/allowed" + r.GET(path, func(c router.Control) { + c.Code(http.StatusOK) + }) + r.SetupNotAllowedHandler(func(c router.Control) { + c.Code(http.StatusForbidden) + c.Write(message) + }) + req, err := http.NewRequest("PUT", path, nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + code := trw.Code + if code != http.StatusForbidden { + t.Error("Expected", http.StatusForbidden, "got", code) + } + header := trw.Header().Get("Allow") + expected := "GET" + if header != expected { + t.Error("Expected", expected, "got", header) + } + if trw.Body.String() != message { + t.Error("Expected", message, "got", trw.Body.String()) + } +} + +func TestRouterSetupNotFound(t *testing.T) { + r := getRouterForTesting() + message := http.StatusText(http.StatusForbidden) + r.SetupNotFoundHandler(func(c router.Control) { + c.Code(http.StatusForbidden) + c.Write(message) + }) + req, err := http.NewRequest("GET", "/not/found", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusForbidden { + t.Error("Expected", http.StatusForbidden, "got", result) + } + if trw.Body.String() != message { + t.Error("Expected", message, "got", trw.Body.String()) + } +} + +func TestRouterRecoveryHandler(t *testing.T) { + r := getRouterForTesting() + message := http.StatusText(http.StatusServiceUnavailable) + path := "/recovery" + r.GET(path, func(c router.Control) { + panic("test") + }) + r.SetupRecoveryHandler(func(c router.Control) { + c.Code(http.StatusServiceUnavailable) + c.Write(message) + }) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusServiceUnavailable { + t.Error("Expected", http.StatusForbidden, "got", result) + } + if trw.Body.String() != message { + t.Error("Expected", message, "got", trw.Body.String()) + } +} + +func TestRouterMiddleware(t *testing.T) { + r := getRouterForTesting() + message := http.StatusText(http.StatusOK) + path := "/middleware" + r.GET(path, func(c router.Control) { + c.Code(http.StatusOK) + c.Write(message) + }) + r.SetupMiddleware(func(f func(router.Control)) func(router.Control) { + return func(c router.Control) { + headers := c.Request().Header.Get("Access-Control-Request-Headers") + if headers != "" { + c.Header().Set("Access-Control-Allow-Headers", "content-type") + } + f(c) + } + }) + req, err := http.NewRequest("GET", path, nil) + req.Header.Set("Access-Control-Request-Headers", "All") + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusOK { + t.Error("Expected", http.StatusOK, "got", result) + } + header := trw.Header().Get("Access-Control-Allow-Headers") + expected := "content-type" + if header != expected { + t.Error("Expected", expected, "got", header) + } + if trw.Body.String() != message { + t.Error("Expected", message, "got", trw.Body.String()) + } +} diff --git a/pkg/router/httprouter.go b/pkg/router/httprouter.go new file mode 100644 index 0000000..a274845 --- /dev/null +++ b/pkg/router/httprouter.go @@ -0,0 +1,56 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package router + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" +) + +// HTTPRouter interface contains base http methods e.g. GET, PUT, POST +// and defines your own handlers that is useful in some use cases +type HTTPRouter interface { + // Standard methods + + // GET registers a new request handle for HTTP GET method. + GET(path string, h httprouter.Handle) + // PUT registers a new request handle for HTTP PUT method. + PUT(path string, h httprouter.Handle) + // POST registers a new request handle for HTTP POST method. + POST(path string, h httprouter.Handle) + // DELETE registers a new request handle for HTTP DELETE method. + DELETE(path string, h httprouter.Handle) + // HEAD registers a new request handle for HTTP HEAD method. + HEAD(path string, h httprouter.Handle) + // OPTIONS registers a new request handle for HTTP OPTIONS method. + OPTIONS(path string, h httprouter.Handle) + // PATCH registers a new request handle for HTTP PATCH method. + PATCH(path string, h httprouter.Handle) + + // User defined options and handlers + + // If enabled, the router automatically replies to OPTIONS requests. + // Nevertheless OPTIONS handlers take priority over automatic replies. + // By default this option is enabled + UseOptionsReplies(bool) + + // SetupNotAllowedHandler defines own handler which is called when a request + // cannot be routed. + SetupNotAllowedHandler(http.Handler) + + // SetupNotFoundHandler allows to define own handler for undefined URL path. + // If it is not set, http.NotFound is used. + SetupNotFoundHandler(http.Handler) + + // SetupRecoveryHandler allows to define handler that called when panic happen. + // The handler prevents your server from crashing and should be used to return + // http status code http.StatusInternalServerError (500) + // interface{} will contain value which is transmitted from panic call. + SetupRecoveryHandler(func(http.ResponseWriter, *http.Request, interface{})) + + // Listen and serve on requested host and port e.g "0.0.0.0:8080" + Listen(hostPort string) error +} diff --git a/pkg/router/httprouter/httprouter_wrapper.go b/pkg/router/httprouter/httprouter_wrapper.go new file mode 100644 index 0000000..4a1fcac --- /dev/null +++ b/pkg/router/httprouter/httprouter_wrapper.go @@ -0,0 +1,58 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package httprouter + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/takama/k8sapp/pkg/router" +) + +type httpRouter struct { + httprouter.Router +} + +// New returns new router that implement HTTPRouter interface. +func New() router.HTTPRouter { + router := new(httpRouter) + router.RedirectTrailingSlash = true + router.RedirectFixedPath = true + router.HandleMethodNotAllowed = true + router.HandleOPTIONS = true + return router +} + +// If enabled, the router automatically replies to OPTIONS requests. +// Nevertheless OPTIONS handlers take priority over automatic replies. +// By default this option is disabled +func (hr *httpRouter) UseOptionsReplies(enabled bool) { + hr.HandleOPTIONS = enabled +} + +// SetupNotAllowedHandler defines own handler which is called when a request +// cannot be routed. +func (hr *httpRouter) SetupNotAllowedHandler(h http.Handler) { + hr.MethodNotAllowed = h +} + +// SetupNotFoundHandler allows to define own handler for undefined URL path. +// If it is not set, http.NotFound is used. +func (hr *httpRouter) SetupNotFoundHandler(h http.Handler) { + hr.NotFound = h +} + +// SetupRecoveryHandler allows to define handler that called when panic happen. +// The handler prevents your server from crashing and should be used to return +// http status code http.StatusInternalServerError (500) +// interface{} will contain value which is transmitted from panic call. +func (hr *httpRouter) SetupRecoveryHandler(f func(http.ResponseWriter, *http.Request, interface{})) { + hr.PanicHandler = f +} + +// Listen and serve on requested host and port e.g "0.0.0.0:8080" +func (hr *httpRouter) Listen(hostPort string) error { + return http.ListenAndServe(hostPort, hr) +} diff --git a/pkg/router/httprouter/httprouter_wrapper_test.go b/pkg/router/httprouter/httprouter_wrapper_test.go new file mode 100644 index 0000000..a3bb72d --- /dev/null +++ b/pkg/router/httprouter/httprouter_wrapper_test.go @@ -0,0 +1,48 @@ +package httprouter + +import ( + "net/http" + "testing" +) + +func TestNewHTTPRouter(t *testing.T) { + r := New() + if r == nil { + t.Error("Expected new httprouter, got nil") + } +} +func TestHTTPRouter(t *testing.T) { + r := new(httpRouter) + if r.HandleOPTIONS { + t.Error("Expected of handle OPTIONS is not set") + } + r.UseOptionsReplies(true) + if !r.HandleOPTIONS { + t.Error("Expected of handle OPTIONS is set") + } + if r.MethodNotAllowed != nil { + t.Error("Expected nil, got", r.MethodNotAllowed) + } + r.SetupNotAllowedHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + if r.MethodNotAllowed == nil { + t.Error("Expected handler, got nil") + } + if r.NotFound != nil { + t.Error("Expected nil, got", r.NotFound) + } + r.SetupNotFoundHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + if r.NotFound == nil { + t.Error("Expected handler, got nil") + } + if r.PanicHandler != nil { + t.Error("Expected nil, got not nil") + } + r.SetupRecoveryHandler(func(http.ResponseWriter, *http.Request, interface{}) {}) + if r.PanicHandler == nil { + t.Error("Expected handler, got nil") + } + err := r.Listen("$") + if err == nil { + t.Error("Expected error if used incorrect host and port") + } +} diff --git a/pkg/service/service.go b/pkg/service/service.go index a12c392..856ad27 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -5,21 +5,39 @@ package service import ( + "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/handlers" "github.com/takama/k8sapp/pkg/logger" + stdlog "github.com/takama/k8sapp/pkg/logger/standard" + "github.com/takama/k8sapp/pkg/router" + "github.com/takama/k8sapp/pkg/router/bitroute" "github.com/takama/k8sapp/pkg/version" ) -// Run starts the service -func Run() (err error) { +// Setup configures the service +func Setup(cfg *config.Config) (r router.BitRoute, err error) { // Setup logger - log := logger.New(&logger.Config{ - Level: logger.LevelDebug, + log := stdlog.New(&logger.Config{ + Level: cfg.LogLevel, Time: true, UTC: true, }) log.Info("Version:", version.RELEASE) log.Warnf("%s log level is used", logger.LevelDebug.String()) + log.Infof("Service %s listened on %s:%d", config.SERVICENAME, cfg.LocalHost, cfg.LocalPort) + + // Define handlers + h := handlers.New(log, cfg) + + // Register new router + r = bitroute.New() + + // Configure router + r.SetupMiddleware(h.Base) + r.GET("/", h.Root) + r.GET("/healthz", h.Health) + r.GET("/readyz", h.Ready) return } diff --git a/pkg/service/service_test.go b/pkg/service/service_test.go index a746455..fa8868e 100644 --- a/pkg/service/service_test.go +++ b/pkg/service/service_test.go @@ -1,10 +1,22 @@ package service -import "testing" +import ( + "testing" + + "github.com/takama/k8sapp/pkg/config" +) func TestRun(t *testing.T) { - err := Run() + cfg := new(config.Config) + err := cfg.Load(config.SERVICENAME) + if err != nil { + t.Error("Expected loading of environment vars, got", err) + } + router, err := Setup(cfg) if err != nil { t.Errorf("Fail, got '%s', want '%v'", err, nil) } + if router == nil { + t.Error("Expected new router, got nil") + } }