diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d318b128d..1bae08057 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,9 +35,19 @@ To build and start `unsee` from local branch see `Running` section of the [README](README.md) file. When working with assets (templates, stylesheets and javascript files) `DEBUG` -flag for make can be set, which will recompile binary assets in debug mode, +make variable can be set, which will recompile binary assets in debug mode, meaning that files from disk will be read instead of compiled in assets. See [go-bindata docs](https://github.com/jteeuwen/go-bindata#debug-vs-release-builds) for details. Example: make DEBUG=true run + make DEBUG=true run-docker + +Note that this is not the same as enabling [debug mode](/README.md#debug) for +the [gin web framework](https://github.com/gin-gonic/gin) which is used +internally, but enabling `DEBUG` via this make variable will also enable gin +debug mode. +When running docker image via `make run-docker` with `DEBUG` make variable set +to `true` volume mapping will be added (in read-only mode), so that unsee +instance running inside the docker can read asset files from the sources +directory. diff --git a/Makefile b/Makefile index 9e9343af3..1748d4788 100644 --- a/Makefile +++ b/Makefile @@ -10,9 +10,12 @@ SOURCES := $(wildcard *.go) $(wildcard */*.go) ASSET_SOURCES := $(wildcard assets/*/* assets/*/*/*) GO_BINDATA_MODE := prod +GIN_DEBUG := false ifdef DEBUG GO_BINDATA_FLAGS = -debug GO_BINDATA_MODE = debug + GIN_DEBUG = true + DOCKER_ARGS = -v $(CURDIR)/assets:$(CURDIR)/assets:ro endif .DEFAULT_GOAL := $(NAME) @@ -42,10 +45,10 @@ clean: .PHONY: run run: $(NAME) - DEBUG=true \ ALERTMANAGER_URI=$(ALERTMANAGER_URI) \ COLOR_LABELS_UNIQUE="instance cluster" \ COLOR_LABELS_STATIC="job" \ + DEBUG="$(GIN_DEBUG)" \ PORT=$(PORT) \ ./$(NAME) @@ -58,9 +61,11 @@ run-docker: docker-image @docker rm -f $(NAME) || true docker run \ --name $(NAME) \ + $(DOCKER_ARGS) \ -e ALERTMANAGER_URI=$(ALERTMANAGER_URI) \ -e COLOR_LABELS_UNIQUE="instance cluster" \ -e COLOR_LABELS_STATIC="job" \ + -e DEBUG="$(GIN_DEBUG)" \ -e PORT=$(PORT) \ -p $(PORT):$(PORT) \ $(NAME):$(VERSION) diff --git a/README.md b/README.md index 3eef28b56..cf37c9759 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,9 @@ This variable is required and there is no default value. #### DEBUG -Will enable [gin](https://github.com/gin-gonic/gin) debug mode. Examples: +Will enable [gin](https://github.com/gin-gonic/gin) debug mode. This will +configure to print out more debugging information on startup. +Examples: DEBUG=true DEBUG=false diff --git a/config/config.go b/config/config.go index 54e90a9f9..4d5ecdda6 100644 --- a/config/config.go +++ b/config/config.go @@ -4,6 +4,7 @@ import ( "bytes" "flag" "fmt" + "net/url" "os" "reflect" "strings" @@ -11,6 +12,7 @@ import ( "unicode" log "github.com/Sirupsen/logrus" + "github.com/asaskevich/govalidator" "github.com/kelseyhightower/envconfig" ) @@ -119,12 +121,35 @@ func (config *configEnvs) Read() { if err != nil { log.Fatal(err) } +} +func hideURLPassword(s string) (string, error) { + u, err := url.Parse(s) + if err != nil { + return "", err + } + if u.User != nil { + if _, pwdSet := u.User.Password(); pwdSet { + u.User = url.UserPassword(u.User.Username(), "xxx") + } + } + return u.String(), nil +} + +func (config *configEnvs) LogValues() { s := reflect.ValueOf(config).Elem() typeOfT := s.Type() for i := 0; i < s.NumField(); i++ { - f := s.Field(i) - log.Infof("%20s => %v", typeOfT.Field(i).Tag.Get("envconfig"), f.Interface()) + env := typeOfT.Field(i).Tag.Get("envconfig") + val := fmt.Sprintf("%v", s.Field(i).Interface()) + if govalidator.IsURL(val) { + var err error + val, err = hideURLPassword(val) + if err != nil { + log.Errorf("Failed to parse url value for %s: %s", env, err.Error()) + } + } + log.Infof("%20s => %v", env, val) } } diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 000000000..bf11fb480 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,99 @@ +package config + +import ( + "os" + "testing" + "time" +) + +type flagNameTest struct { + env string + flag string +} + +var flagNameTests = []flagNameTest{ + flagNameTest{env: "MyEnv", flag: "my.env"}, + flagNameTest{env: "MyENV", flag: "my.env"}, + flagNameTest{env: "MYEnv", flag: "myenv"}, +} + +func TestMakeFlagName(t *testing.T) { + for _, testCase := range flagNameTests { + generatedFlag := makeFlagName(testCase.env) + if generatedFlag != testCase.flag { + t.Errorf("Invalid flag name generated from env '%s', expected '%s', got '%s'", testCase.env, testCase.flag, generatedFlag) + } + } +} + +func stringInSlice(stringArray []string, value string) bool { + for _, s := range stringArray { + if s == value { + return true + } + } + return false +} + +func TestReadConfig(t *testing.T) { + os.Setenv("ALERTMANAGER_TTL", "1s") + os.Setenv("ALERTMANAGER_URI", "http://localhost") + os.Setenv("DEBUG", "true") + os.Setenv("COLOR_LABELS_STATIC", "a bb ccc") + Config.Read() + if Config.AlertmanagerTTL != time.Second { + t.Errorf("Config.AlertmanagerTTL is invalid, expected 1s, got %v", Config.AlertmanagerTTL) + } + if Config.Debug != true { + t.Errorf("Config.Debug is %v with env DEBUG=true set", Config.Debug) + } + if !stringInSlice(Config.ColorLabelsStatic, "a") { + t.Errorf("Config.ColorLabelsStatic is missing value 'a': %v", Config.ColorLabelsStatic) + } + if !stringInSlice(Config.ColorLabelsStatic, "bb") { + t.Errorf("Config.ColorLabelsStatic is missing value 'bb': %v", Config.ColorLabelsStatic) + } + if !stringInSlice(Config.ColorLabelsStatic, "ccc") { + t.Errorf("Config.ColorLabelsStatic is missing value 'ccc': %v", Config.ColorLabelsStatic) + } + if Config.Port != 8080 { + t.Errorf("Config.Port is invalid, expected 8080, got %v", Config.Port) + } + +} + +type urlSecretTest struct { + raw string + sanitized string +} + +var urlSecretTests = []urlSecretTest{ + urlSecretTest{ + raw: "http://localhost", + sanitized: "http://localhost", + }, + urlSecretTest{ + raw: "http://alertmanager.example.com/path", + sanitized: "http://alertmanager.example.com/path", + }, + urlSecretTest{ + raw: "http://user@alertmanager.example.com/path", + sanitized: "http://user@alertmanager.example.com/path", + }, + urlSecretTest{ + raw: "https://user:password@alertmanager.example.com/path", + sanitized: "https://user:xxx@alertmanager.example.com/path", + }, +} + +func TestUrlSecretTest(t *testing.T) { + for _, testCase := range urlSecretTests { + sanitized, err := hideURLPassword(testCase.raw) + if err != nil { + t.Errorf("Unexpected error when parsing '%s': %s", testCase.raw, err.Error()) + } + if sanitized != testCase.sanitized { + t.Errorf("Invalid sanitized url, expected '%s', got '%s'", testCase.sanitized, sanitized) + } + } +} diff --git a/main.go b/main.go index d032639f6..724429605 100644 --- a/main.go +++ b/main.go @@ -70,8 +70,6 @@ func init() { } func setupRouter(router *gin.Engine) { - router.SetHTMLTemplate(loadTemplates("templates")) - router.Use(static.Serve("/static", newBinaryFileSystem("static"))) router.GET("/favicon.ico", favicon) @@ -85,6 +83,7 @@ func main() { log.Infof("Version: %s", version) config.Config.Read() + config.Config.LogValues() transform.ParseRules(config.Config.JiraRegexp) apiCache = cache.New(cache.NoExpiration, 10*time.Second) @@ -106,7 +105,7 @@ func main() { } router := gin.New() - setupRouter(router) + router.SetHTMLTemplate(loadTemplates("templates")) prom := ginprometheus.NewPrometheus("gin") prom.Use(router) @@ -120,5 +119,6 @@ func main() { router.Use(sentry.Recovery(raven.DefaultClient, false)) } + setupRouter(router) router.Run() } diff --git a/views.go b/views.go index bd9d74cc2..3f08a47c4 100644 --- a/views.go +++ b/views.go @@ -4,9 +4,6 @@ import ( "crypto/sha1" "encoding/json" "fmt" - "github.com/cloudflare/unsee/config" - "github.com/cloudflare/unsee/models" - "github.com/cloudflare/unsee/store" "io" "net/http" "sort" @@ -14,10 +11,19 @@ import ( "strings" "time" + "github.com/cloudflare/unsee/config" + "github.com/cloudflare/unsee/models" + "github.com/cloudflare/unsee/store" + log "github.com/Sirupsen/logrus" "github.com/gin-gonic/gin" ) +var ( + // needed for serving favicon from binary assets + faviconFileServer = http.FileServer(newBinaryFileSystem("static")) +) + func boolInSlice(boolArray []bool, value bool) bool { for _, s := range boolArray { if s == value { @@ -262,7 +268,5 @@ func autocomplete(c *gin.Context) { } func favicon(c *gin.Context) { - fs := newBinaryFileSystem("static") - fileserver := http.FileServer(fs) - fileserver.ServeHTTP(c.Writer, c.Request) + faviconFileServer.ServeHTTP(c.Writer, c.Request) } diff --git a/views_test.go b/views_test.go index 2983f8312..5023176f8 100644 --- a/views_test.go +++ b/views_test.go @@ -29,6 +29,7 @@ func mockConfig() { func ginTestEngine() *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() + r.SetHTMLTemplate(loadTemplates("templates")) setupRouter(r) return r } @@ -256,6 +257,14 @@ var acTests = []acTestCase{ "node=localhost", }, }, + // duplicated to test reponse caching + acTestCase{ + Term: "Nod", + Results: []string{ + "node!=localhost", + "node=localhost", + }, + }, } func TestAutocomplete(t *testing.T) { @@ -294,3 +303,45 @@ func TestAutocomplete(t *testing.T) { } } } + +type staticFileTestCase struct { + path string + code int +} + +var staticFileTests = []staticFileTestCase{ + staticFileTestCase{ + path: "/favicon.ico", + code: 200, + }, + staticFileTestCase{ + path: "/static/unsee.js", + code: 200, + }, + staticFileTestCase{ + path: "/static/managed/js/assets.txt", + code: 200, + }, + staticFileTestCase{ + path: "/xxx", + code: 404, + }, + staticFileTestCase{ + path: "/static/abcd", + code: 404, + }, +} + +func TestStaticFiles(t *testing.T) { + mockConfig() + mockAlerts() + r := ginTestEngine() + for _, staticFileTest := range staticFileTests { + req, _ := http.NewRequest("GET", staticFileTest.path, nil) + resp := httptest.NewRecorder() + r.ServeHTTP(resp, req) + if resp.Code != staticFileTest.code { + t.Errorf("Invalid status code for GET %s: %d", staticFileTest.path, resp.Code) + } + } +}