From 29f4e0a5d17ac52a111921fbb0fd665753fa3003 Mon Sep 17 00:00:00 2001 From: Eirik Martiniussen Sylliaas Date: Sun, 7 Jan 2018 16:30:36 +0100 Subject: [PATCH 1/5] Use cobra and viper to build a better cli entrypoint --- Gopkg.lock | 80 ++++++++++++++++++++- cmd/cli.go | 26 +++++++ cmd/root.go | 15 ++++ cmd/server.go | 118 +++++++++++++++++++++++++++++++ main.go | 100 ++------------------------ {src => pkg}/incidents.go | 2 +- {src => pkg}/routes/api.go | 36 +++++----- {src => pkg}/routes/dashboard.go | 12 ++-- {src => pkg}/services.go | 2 +- {src => pkg}/utils.go | 10 ++- {src => pkg}/utils_test.go | 2 +- {src => pkg}/validators.go | 2 +- 12 files changed, 280 insertions(+), 125 deletions(-) create mode 100644 cmd/cli.go create mode 100644 cmd/root.go create mode 100644 cmd/server.go rename {src => pkg}/incidents.go (99%) rename {src => pkg}/routes/api.go (84%) rename {src => pkg}/routes/dashboard.go (68%) rename {src => pkg}/services.go (99%) rename {src => pkg}/utils.go (92%) rename {src => pkg}/utils_test.go (96%) rename {src => pkg}/validators.go (98%) diff --git a/Gopkg.lock b/Gopkg.lock index 4c33d7a..038360c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,12 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + name = "github.com/fsnotify/fsnotify" + packages = ["."] + revision = "629574ca2a5df945712d3079857300b5e4da0236" + version = "v1.4.2" + [[projects]] branch = "master" name = "github.com/gin-contrib/sse" @@ -25,6 +31,18 @@ packages = ["proto"] revision = "130e6b02ab059e7b717a096f397c5b60111cae74" +[[projects]] + branch = "master" + name = "github.com/hashicorp/hcl" + packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"] + revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" + +[[projects]] + name = "github.com/inconshreveable/mousetrap" + packages = ["."] + revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" + version = "v1.0" + [[projects]] branch = "master" name = "github.com/jinzhu/inflection" @@ -37,12 +55,66 @@ revision = "6ed27152e0428abfde127acb33b08b03a1e67cac" version = "1.0.2" +[[projects]] + name = "github.com/magiconair/properties" + packages = ["."] + revision = "d419a98cdbed11a922bf76f257b7c4be79b50e73" + version = "v1.7.4" + [[projects]] name = "github.com/mattn/go-isatty" packages = ["."] revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" version = "v0.0.3" +[[projects]] + branch = "master" + name = "github.com/mitchellh/mapstructure" + packages = ["."] + revision = "06020f85339e21b2478f756a78e295255ffa4d6a" + +[[projects]] + name = "github.com/pelletier/go-toml" + packages = ["."] + revision = "16398bac157da96aa88f98a2df640c7f32af1da2" + version = "v1.0.1" + +[[projects]] + name = "github.com/spf13/afero" + packages = [".","mem"] + revision = "ec3a3111d1e1bdff38a61e09d5a5f5e974905611" + version = "v1.0.1" + +[[projects]] + name = "github.com/spf13/cast" + packages = ["."] + revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4" + version = "v1.1.0" + +[[projects]] + name = "github.com/spf13/cobra" + packages = ["."] + revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" + version = "v0.0.1" + +[[projects]] + branch = "master" + name = "github.com/spf13/jwalterweatherman" + packages = ["."] + revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b" + +[[projects]] + name = "github.com/spf13/pflag" + packages = ["."] + revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" + version = "v1.0.0" + +[[projects]] + name = "github.com/spf13/viper" + packages = ["."] + revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" + version = "v1.0.0" + [[projects]] branch = "master" name = "github.com/ugorji/go" @@ -55,6 +127,12 @@ packages = ["unix"] revision = "314a259e304ff91bd6985da2a7149bbf91237993" +[[projects]] + branch = "master" + name = "golang.org/x/text" + packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"] + revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" + [[projects]] name = "gopkg.in/go-playground/validator.v8" packages = ["."] @@ -70,6 +148,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "4544073386bd8b7f1b7dac2351ee8b7e0aae1d8e408659078e4361597e6bd84e" + inputs-digest = "0f5003afd9b899e77a0885d497f248226a971b8a6f837c63b77aee4c4385c965" solver-name = "gps-cdcl" solver-version = 1 diff --git a/cmd/cli.go b/cmd/cli.go new file mode 100644 index 0000000..1198578 --- /dev/null +++ b/cmd/cli.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func init() { + cliCmd.PersistentFlags().StringP("apiUrl", "a", "http://127.0.0.1:8080", "The URL used to access the API") + cliCmd.PersistentFlags().StringP("token", "t", "", "The token used for authorizing with the API") + viper.BindPFlag("apiUrl", cliCmd.PersistentFlags().Lookup("apiUrl")) + viper.BindPFlag("token", cliCmd.PersistentFlags().Lookup("token")) + viper.BindEnv("apiUrl", "API_URL") + viper.BindEnv("token", "TOKEN") + RootCmd.AddCommand(cliCmd) +} + +var cliCmd = &cobra.Command{ + Use: "cli", + Short: "Access the statuspage api from the command line", + Run: func(cmd *cobra.Command, args []string) { + apiUrl, token := viper.GetString("apiUrl"), viper.GetString("token") + fmt.Print(apiUrl, token) + }, +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..11bbb63 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func init() { + +} + +var RootCmd = &cobra.Command{ + Use: "statuspage", + Short: "Statuspage platform written in golang with postgres as the backing datastore.", + Long: ``, +} diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 0000000..956d60a --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,118 @@ +package cmd + +import ( + "github.com/eirsyl/statuspage/pkg" + "github.com/eirsyl/statuspage/pkg/routes" + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/go-pg/pg" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "net/http" + "os" +) + +func init() { + serverCmd.PersistentFlags().StringP("token", "t", "", "The token used for authorizing with the API") + serverCmd.PersistentFlags().StringP("postgresAddress", "a", "statuspage", "The address postgres is listening on") + serverCmd.PersistentFlags().StringP("postgresUser", "u", "statuspage", "The user statuspage should connect to postgres as") + serverCmd.PersistentFlags().StringP("postgresPassword", "p", "", "The postgres password") + serverCmd.PersistentFlags().StringP("postgresDatabase", "d", "statuspage", "The postgres database statuspage should use") + viper.BindPFlag("token", serverCmd.PersistentFlags().Lookup("token")) + viper.BindPFlag("postgresAddress", serverCmd.PersistentFlags().Lookup("postgresAddress")) + viper.BindPFlag("postgresUser", serverCmd.PersistentFlags().Lookup("postgresUser")) + viper.BindPFlag("postgresPassword", serverCmd.PersistentFlags().Lookup("postgresPassword")) + viper.BindPFlag("postgresDatabase", serverCmd.PersistentFlags().Lookup("postgresDatabase")) + viper.BindEnv("token", "TOKEN") + viper.BindEnv("postgresAddress", "POSTGRES_ADDRESS") + viper.BindEnv("postgresUser", "POSTGRES_USER") + viper.BindEnv("postgresPassword", "POSTGRES_PASSWORD") + viper.BindEnv("postgresDatabase", "POSTGRES_DATABASE") + RootCmd.AddCommand(serverCmd) +} + +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Start the statuspage server", + Run: func(cmd *cobra.Command, args []string) { + pkg.ConfigRuntime() + gin.SetMode(gin.ReleaseMode) + + router := gin.Default() + router.Use(State()) + + binding.Validator.RegisterValidation("incidentstatus", pkg.IncidentStatus) + binding.Validator.RegisterValidation("servicestatus", pkg.ServiceStatus) + + router.Static("/static", "./static") + router.LoadHTMLGlob("templates/*") + + router.GET("/", routes.Dashboard) + + api := router.Group("/api") + api.Use(Auth()) + { + api.GET("/services", routes.ServiceList) + api.POST("/services", routes.ServicePost) + api.GET("/services/:id", routes.ServiceGet) + api.PATCH("/services/:id", routes.ServicePatch) + api.DELETE("/services/:id", routes.ServiceDelete) + + api.GET("/incidents", routes.IncidentList) + api.POST("/incidents", routes.IncidentPost) + api.GET("/incidents/:id", routes.IncidentGet) + api.DELETE("/incidents/:id", routes.IncidentDelete) + + api.GET("/incidents/:id/updates", routes.IncidentUpdateList) + api.POST("/incidents/:id/updates", routes.IncidentUpdatePost) + api.GET("/incidents/:id/updates/:updateId", routes.IncidentUpdateGet) + api.DELETE("/incidents/:id/updates/:updateId", routes.IncidentUpdateDelete) + } + + router.Run() + }, +} + +func State() gin.HandlerFunc { + pgAddr := os.Getenv("POSTGRES_ADDRESS") + pgUser := os.Getenv("POSTGRES_USER") + pgPassword := os.Getenv("POSTGRES_PASSWORD") + pgDB := os.Getenv("POSTGRES_DB") + + db := pg.Connect(&pg.Options{ + Addr: pgAddr, + User: pgUser, + Password: pgPassword, + Database: pgDB, + }) + + if err := pkg.CreateSchema(db); err != nil { + panic(err) + } + + services := pkg.Services{} + services.Initialize(*db) + + incidents := pkg.Incidents{} + incidents.Initialize(*db) + + return func(c *gin.Context) { + c.Set("services", services) + c.Set("incidents", incidents) + c.Next() + } +} + +func Auth() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + validToken := os.Getenv("API_TOKEN") + + if !(len(validToken) > 0 && token == validToken) { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + c.Next() + } +} diff --git a/main.go b/main.go index dc64244..dca56a3 100644 --- a/main.go +++ b/main.go @@ -1,104 +1,14 @@ package main import ( - "github.com/eirsyl/statuspage/src" - "github.com/eirsyl/statuspage/src/routes" - "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/binding" - "github.com/go-pg/pg" - "log" - "net/http" + "fmt" + "github.com/eirsyl/statuspage/cmd" "os" - "runtime" ) func main() { - - ConfigRuntime() - - gin.SetMode(gin.ReleaseMode) - - router := gin.Default() - router.Use(State()) - - binding.Validator.RegisterValidation("incidentstatus", src.IncidentStatus) - binding.Validator.RegisterValidation("servicestatus", src.ServiceStatus) - - router.Static("/static", "./static") - router.LoadHTMLGlob("templates/*") - - router.GET("/", routes.Dashboard) - - api := router.Group("/api") - api.Use(Auth()) - { - api.GET("/services", routes.ServiceList) - api.POST("/services", routes.ServicePost) - api.GET("/services/:id", routes.ServiceGet) - api.PATCH("/services/:id", routes.ServicePatch) - api.DELETE("/services/:id", routes.ServiceDelete) - - api.GET("/incidents", routes.IncidentList) - api.POST("/incidents", routes.IncidentPost) - api.GET("/incidents/:id", routes.IncidentGet) - api.DELETE("/incidents/:id", routes.IncidentDelete) - - api.GET("/incidents/:id/updates", routes.IncidentUpdateList) - api.POST("/incidents/:id/updates", routes.IncidentUpdatePost) - api.GET("/incidents/:id/updates/:updateId", routes.IncidentUpdateGet) - api.DELETE("/incidents/:id/updates/:updateId", routes.IncidentUpdateDelete) - } - - router.Run() - -} - -func ConfigRuntime() { - nuCPU := runtime.NumCPU() - runtime.GOMAXPROCS(nuCPU) - log.Printf("Running with %d CPUs\n", nuCPU) -} - -func State() gin.HandlerFunc { - pgAddr := os.Getenv("POSTGRES_ADDRESS") - pgUser := os.Getenv("POSTGRES_USER") - pgPassword := os.Getenv("POSTGRES_PASSWORD") - pgDB := os.Getenv("POSTGRES_DB") - - db := pg.Connect(&pg.Options{ - Addr: pgAddr, - User: pgUser, - Password: pgPassword, - Database: pgDB, - }) - - if err := src.CreateSchema(db); err != nil { - panic(err) - } - - services := src.Services{} - services.Initialize(*db) - - incidents := src.Incidents{} - incidents.Initialize(*db) - - return func(c *gin.Context) { - c.Set("services", services) - c.Set("incidents", incidents) - c.Next() - } -} - -func Auth() gin.HandlerFunc { - return func(c *gin.Context) { - token := c.GetHeader("Authorization") - validToken := os.Getenv("API_TOKEN") - - if !(len(validToken) > 0 && token == validToken) { - c.AbortWithStatus(http.StatusUnauthorized) - return - } - - c.Next() + if err := cmd.RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) } } diff --git a/src/incidents.go b/pkg/incidents.go similarity index 99% rename from src/incidents.go rename to pkg/incidents.go index a6a6373..17014f7 100644 --- a/src/incidents.go +++ b/pkg/incidents.go @@ -1,4 +1,4 @@ -package src +package pkg import ( "github.com/go-pg/pg" diff --git a/src/routes/api.go b/pkg/routes/api.go similarity index 84% rename from src/routes/api.go rename to pkg/routes/api.go index 0e75bdd..0da282d 100644 --- a/src/routes/api.go +++ b/pkg/routes/api.go @@ -1,7 +1,7 @@ package routes import ( - "github.com/eirsyl/statuspage/src" + "github.com/eirsyl/statuspage/pkg" "github.com/gin-gonic/gin" "net/http" "strconv" @@ -12,7 +12,7 @@ import ( */ func ServiceList(c *gin.Context) { - services := c.Keys["services"].(src.Services) + services := c.Keys["services"].(pkg.Services) s, err := services.GetServices() if err != nil { @@ -23,14 +23,14 @@ func ServiceList(c *gin.Context) { } func ServicePost(c *gin.Context) { - var service src.Service + var service pkg.Service err := c.BindJSON(&service) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - services := c.Keys["services"].(src.Services) + services := c.Keys["services"].(pkg.Services) err = services.InsertService(&service) if err != nil { @@ -49,7 +49,7 @@ func ServiceGet(c *gin.Context) { return } - services := c.Keys["services"].(src.Services) + services := c.Keys["services"].(pkg.Services) s, err := services.GetService(int64(id)) if err != nil { @@ -68,14 +68,14 @@ func ServicePatch(c *gin.Context) { return } - var service src.Service + var service pkg.Service err = c.BindJSON(&service) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - services := c.Keys["services"].(src.Services) + services := c.Keys["services"].(pkg.Services) err = services.UpdateService(int64(id), &service) if err != nil { @@ -94,7 +94,7 @@ func ServiceDelete(c *gin.Context) { return } - services := c.Keys["services"].(src.Services) + services := c.Keys["services"].(pkg.Services) err = services.DeleteService(int64(id)) if err != nil { @@ -110,7 +110,7 @@ func ServiceDelete(c *gin.Context) { */ func IncidentList(c *gin.Context) { - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) i, err := incidents.GetLatestIncidents() if err != nil { @@ -121,7 +121,7 @@ func IncidentList(c *gin.Context) { } func IncidentPost(c *gin.Context) { - var incident src.Incident + var incident pkg.Incident err := c.BindJSON(&incident) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -130,7 +130,7 @@ func IncidentPost(c *gin.Context) { incident.Updates = nil - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) err = incidents.InsertIncident(&incident) if err != nil { @@ -149,7 +149,7 @@ func IncidentGet(c *gin.Context) { return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) i, err := incidents.GetIncident(int64(id)) if err != nil { @@ -168,7 +168,7 @@ func IncidentDelete(c *gin.Context) { return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) err = incidents.DeleteIncident(int64(id)) @@ -188,7 +188,7 @@ func IncidentUpdateList(c *gin.Context) { return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) incident, err := incidents.GetIncident(int64(id)) @@ -208,14 +208,14 @@ func IncidentUpdatePost(c *gin.Context) { return } - var incidentUpdate src.IncidentUpdate + var incidentUpdate pkg.IncidentUpdate err = c.BindJSON(&incidentUpdate) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) err = incidents.InsertIncidentUpdate(int64(id), &incidentUpdate) @@ -235,7 +235,7 @@ func IncidentUpdateGet(c *gin.Context) { return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) incidentUpdate, err := incidents.GetIncidentUpdate(int64(id)) @@ -255,7 +255,7 @@ func IncidentUpdateDelete(c *gin.Context) { return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) err = incidents.DeleteIncidentUpdate(int64(id)) diff --git a/src/routes/dashboard.go b/pkg/routes/dashboard.go similarity index 68% rename from src/routes/dashboard.go rename to pkg/routes/dashboard.go index 2c89799..f6aee73 100644 --- a/src/routes/dashboard.go +++ b/pkg/routes/dashboard.go @@ -1,15 +1,15 @@ package routes import ( - "github.com/eirsyl/statuspage/src" + "github.com/eirsyl/statuspage/pkg" "github.com/gin-gonic/gin" "net/http" "os" ) func Dashboard(c *gin.Context) { - services := c.Keys["services"].(src.Services) - incidents := c.Keys["incidents"].(src.Incidents) + services := c.Keys["services"].(pkg.Services) + incidents := c.Keys["incidents"].(pkg.Incidents) res, err := services.GetServices() if err != nil { @@ -40,8 +40,8 @@ func Dashboard(c *gin.Context) { "owner": owner, "backgroundColor": color, "logo": logo, - "services": src.AggregateServices(res), - "mostCriticalStatus": src.MostCriticalStatus(res), - "incidents": src.AggregateIncidents(inc), + "services": pkg.AggregateServices(res), + "mostCriticalStatus": pkg.MostCriticalStatus(res), + "incidents": pkg.AggregateIncidents(inc), }) } diff --git a/src/services.go b/pkg/services.go similarity index 99% rename from src/services.go rename to pkg/services.go index 8a1b856..e89f003 100644 --- a/src/services.go +++ b/pkg/services.go @@ -1,4 +1,4 @@ -package src +package pkg import ( "github.com/go-pg/pg" diff --git a/src/utils.go b/pkg/utils.go similarity index 92% rename from src/utils.go rename to pkg/utils.go index 374d7f9..daabbbe 100644 --- a/src/utils.go +++ b/pkg/utils.go @@ -1,11 +1,19 @@ -package src +package pkg import ( "github.com/go-pg/pg" "github.com/go-pg/pg/orm" + "log" + "runtime" "time" ) +func ConfigRuntime() { + nuCPU := runtime.NumCPU() + runtime.GOMAXPROCS(nuCPU) + log.Printf("Running with %d CPUs\n", nuCPU) +} + /* * Aggregate and group services by service group. */ diff --git a/src/utils_test.go b/pkg/utils_test.go similarity index 96% rename from src/utils_test.go rename to pkg/utils_test.go index 992eac0..095d175 100644 --- a/src/utils_test.go +++ b/pkg/utils_test.go @@ -1,4 +1,4 @@ -package src +package pkg import ( "testing" diff --git a/src/validators.go b/pkg/validators.go similarity index 98% rename from src/validators.go rename to pkg/validators.go index 7c059c2..b6627c7 100644 --- a/src/validators.go +++ b/pkg/validators.go @@ -1,4 +1,4 @@ -package src +package pkg import ( "gopkg.in/go-playground/validator.v8" From e8385f80cdf0958edc3d3480b5ef80bbcd648b55 Mon Sep 17 00:00:00 2001 From: Eirik Martiniussen Sylliaas Date: Sun, 7 Jan 2018 18:00:56 +0100 Subject: [PATCH 2/5] Refactor server to match the new folder structure --- .dockerignore | 2 +- .drone.yml | 4 +-- .gitignore | 2 +- Dockerfile | 2 +- cmd/server.go | 68 +++++++++++------------------------------ docker-compose.yaml | 6 ++-- pkg/middleware.go | 53 ++++++++++++++++++++++++++++++++ pkg/routes/dashboard.go | 14 ++++----- 8 files changed, 85 insertions(+), 66 deletions(-) create mode 100644 pkg/middleware.go diff --git a/.dockerignore b/.dockerignore index 3c266c2..7e32383 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,4 +15,4 @@ .git/ vendor/ -statuspage \ No newline at end of file +statuspage diff --git a/.drone.yml b/.drone.yml index c8ee33f..c92fe3b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,5 +8,5 @@ pipeline: commands: - go get -u github.com/golang/dep/cmd/dep - /go/bin/dep ensure - - go test . ./src/... - - go build \ No newline at end of file + - go test . ./pkg/... + - go build diff --git a/.gitignore b/.gitignore index ca2254d..b003e51 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ .glide/ vendor/ -statuspage \ No newline at end of file +statuspage diff --git a/Dockerfile b/Dockerfile index 8aaeb12..4834fe2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,4 +10,4 @@ RUN set -e \ && dep ensure \ && go build -CMD ["./statuspage"] \ No newline at end of file +CMD ["./statuspage", "server"] diff --git a/cmd/server.go b/cmd/server.go index 956d60a..1cbb8ec 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -5,29 +5,38 @@ import ( "github.com/eirsyl/statuspage/pkg/routes" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" - "github.com/go-pg/pg" "github.com/spf13/cobra" "github.com/spf13/viper" - "net/http" - "os" ) func init() { + serverCmd.PersistentFlags().StringP("listenAddress", "l", ":8080", "The address the server should listen on") serverCmd.PersistentFlags().StringP("token", "t", "", "The token used for authorizing with the API") - serverCmd.PersistentFlags().StringP("postgresAddress", "a", "statuspage", "The address postgres is listening on") + serverCmd.PersistentFlags().StringP("postgresAddress", "a", "127.0.0.1:5432", "The address postgres is listening on") serverCmd.PersistentFlags().StringP("postgresUser", "u", "statuspage", "The user statuspage should connect to postgres as") serverCmd.PersistentFlags().StringP("postgresPassword", "p", "", "The postgres password") serverCmd.PersistentFlags().StringP("postgresDatabase", "d", "statuspage", "The postgres database statuspage should use") + serverCmd.PersistentFlags().StringP("siteOwner", "", "Statuspage", "Site owner, used by the html templates") + serverCmd.PersistentFlags().StringP("siteColor", "", "#343434", "The top color used in the templates") + serverCmd.PersistentFlags().StringP("siteLogo", "", "static/img/logo.png", "Path to logo") + viper.BindPFlag("listenAddress", serverCmd.PersistentFlags().Lookup("listenAddress")) viper.BindPFlag("token", serverCmd.PersistentFlags().Lookup("token")) viper.BindPFlag("postgresAddress", serverCmd.PersistentFlags().Lookup("postgresAddress")) viper.BindPFlag("postgresUser", serverCmd.PersistentFlags().Lookup("postgresUser")) viper.BindPFlag("postgresPassword", serverCmd.PersistentFlags().Lookup("postgresPassword")) viper.BindPFlag("postgresDatabase", serverCmd.PersistentFlags().Lookup("postgresDatabase")) + viper.BindPFlag("siteOwner", serverCmd.PersistentFlags().Lookup("siteOwner")) + viper.BindPFlag("siteColor", serverCmd.PersistentFlags().Lookup("siteColor")) + viper.BindPFlag("siteLogo", serverCmd.PersistentFlags().Lookup("siteLogo")) + viper.BindEnv("listenAddress", "LISTEN_ADDRESS") viper.BindEnv("token", "TOKEN") viper.BindEnv("postgresAddress", "POSTGRES_ADDRESS") viper.BindEnv("postgresUser", "POSTGRES_USER") viper.BindEnv("postgresPassword", "POSTGRES_PASSWORD") viper.BindEnv("postgresDatabase", "POSTGRES_DATABASE") + viper.BindEnv("siteOwner", "SITE_OWNER") + viper.BindEnv("siteColor", "SITE_COLOR") + viper.BindEnv("siteLogo", "SITE_LOGO") RootCmd.AddCommand(serverCmd) } @@ -39,7 +48,7 @@ var serverCmd = &cobra.Command{ gin.SetMode(gin.ReleaseMode) router := gin.Default() - router.Use(State()) + router.Use(pkg.State()) binding.Validator.RegisterValidation("incidentstatus", pkg.IncidentStatus) binding.Validator.RegisterValidation("servicestatus", pkg.ServiceStatus) @@ -50,7 +59,7 @@ var serverCmd = &cobra.Command{ router.GET("/", routes.Dashboard) api := router.Group("/api") - api.Use(Auth()) + api.Use(pkg.Auth()) { api.GET("/services", routes.ServiceList) api.POST("/services", routes.ServicePost) @@ -69,50 +78,7 @@ var serverCmd = &cobra.Command{ api.DELETE("/incidents/:id/updates/:updateId", routes.IncidentUpdateDelete) } - router.Run() + listenAddress := viper.GetString("listenAddress") + router.Run(listenAddress) }, } - -func State() gin.HandlerFunc { - pgAddr := os.Getenv("POSTGRES_ADDRESS") - pgUser := os.Getenv("POSTGRES_USER") - pgPassword := os.Getenv("POSTGRES_PASSWORD") - pgDB := os.Getenv("POSTGRES_DB") - - db := pg.Connect(&pg.Options{ - Addr: pgAddr, - User: pgUser, - Password: pgPassword, - Database: pgDB, - }) - - if err := pkg.CreateSchema(db); err != nil { - panic(err) - } - - services := pkg.Services{} - services.Initialize(*db) - - incidents := pkg.Incidents{} - incidents.Initialize(*db) - - return func(c *gin.Context) { - c.Set("services", services) - c.Set("incidents", incidents) - c.Next() - } -} - -func Auth() gin.HandlerFunc { - return func(c *gin.Context) { - token := c.GetHeader("Authorization") - validToken := os.Getenv("API_TOKEN") - - if !(len(validToken) > 0 && token == validToken) { - c.AbortWithStatus(http.StatusUnauthorized) - return - } - - c.Next() - } -} diff --git a/docker-compose.yaml b/docker-compose.yaml index b2bf6c3..e84d016 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,7 +13,7 @@ services: build: context: . ports: - - 80:8080 + - 8080:8080 links: - postgres:postgres environment: @@ -21,8 +21,8 @@ services: - POSTGRES_ADDRESS=postgres:5432 # The address of the postgres instance - POSTGRES_USER=statuspage # The postgres username for authorization - POSTGRES_PASSWORD=statuspage # The postgres password for authorization - - POSTGRES_DB=statuspage # The postgres db name - - SITE_OWNER="DEMO" # The owner of the side, visible in page title + - POSTGRES_DATABASE=statuspage # The postgres db name + - SITE_OWNER=DEMO # The owner of the side, visible in page title - SITE_COLOR= # The background color applied on the header element - SITE_LOGO= # Custom logo, served from another site or local path inside the static folder depends_on: diff --git a/pkg/middleware.go b/pkg/middleware.go new file mode 100644 index 0000000..882eccb --- /dev/null +++ b/pkg/middleware.go @@ -0,0 +1,53 @@ +package pkg + +import ( + "github.com/gin-gonic/gin" + "github.com/go-pg/pg" + "github.com/spf13/viper" + "net/http" +) + +// Auth enforces token authentication on the api endpoints. +func Auth() gin.HandlerFunc { + validToken := viper.GetString("token") + + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + + if validToken == "" || token != validToken { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + c.Next() + } +} + +// State provides the handler with access to the datastore +func State() gin.HandlerFunc { + pgUser, pgPassword := viper.GetString("postgresUser"), viper.GetString("postgresPassword") + pgAddress, pgDatabase := viper.GetString("postgresAddress"), viper.GetString("postgresDatabase") + + db := pg.Connect(&pg.Options{ + Addr: pgAddress, + User: pgUser, + Password: pgPassword, + Database: pgDatabase, + }) + + if err := CreateSchema(db); err != nil { + panic(err) + } + + services := Services{} + services.Initialize(*db) + + incidents := Incidents{} + incidents.Initialize(*db) + + return func(c *gin.Context) { + c.Set("services", services) + c.Set("incidents", incidents) + c.Next() + } +} diff --git a/pkg/routes/dashboard.go b/pkg/routes/dashboard.go index f6aee73..b1629f9 100644 --- a/pkg/routes/dashboard.go +++ b/pkg/routes/dashboard.go @@ -3,8 +3,9 @@ package routes import ( "github.com/eirsyl/statuspage/pkg" "github.com/gin-gonic/gin" + "github.com/spf13/viper" + "log" "net/http" - "os" ) func Dashboard(c *gin.Context) { @@ -21,19 +22,18 @@ func Dashboard(c *gin.Context) { panic(err) } - owner := os.Getenv("SITE_OWNER") + owner, color, logo := viper.GetString("siteOwner"), viper.GetString("siteColor"), viper.GetString("siteLogo") + if owner == "" { - owner = "Abakus" + log.Fatal("The owner cannot be empty") } - color := os.Getenv("SITE_COLOR") if color == "" { - color = "#343434" + log.Fatal("The color cannot be empty") } - logo := os.Getenv("SITE_LOGO") if logo == "" { - logo = "static/img/logo.png" + log.Fatal("The logo cannot be empty") } c.HTML(http.StatusOK, "index.tmpl", gin.H{ From 2832ceb42df66cd0594cc3f26cc62fddb76ef8c4 Mon Sep 17 00:00:00 2001 From: Eirik Martiniussen Sylliaas Date: Sun, 7 Jan 2018 18:10:23 +0100 Subject: [PATCH 3/5] Update readme, fix env variable drescription and add information about the CLI --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9467582..bccabfe 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,18 @@ We use environment variables for configuration, this also includes the logo. ## Configuration We use environment variables to configure the service. The table bellow contains -all variables you can use. +all variables you can use. It's also possible to configure the service using +flags, camelCase is used here. Run `./statuspage server --help for more +information`. |Name |Description| |-----------------|-----------| -|API_TOKEN |This is the token clients should use to access the API (AUTHORIZATION header)| +|LISTEN_ADDRESS |The address the server should listen on| +|TOKEN |This is the token clients should use to access the API (AUTHORIZATION header)| |POSTGRES_ADDRESS |The address of the postgres instance| |POSTGRES_USER |The postgres username for authorization| |POSTGRES_PASSWORD|The postgres password for authorization| -|POSTGRES_DB |The postgres db name| +|POSTGRES_DATABASE|The postgres db name| |SITE_OWNER |The owner of the side, visible in page title| |SITE_COLOR |The background color applied on the header element| |SITE_LOGO |Custom logo, served from another site or local path inside the static folder| @@ -35,3 +38,11 @@ Create new incident ```bash curl -H "Authorization: 123" -v -d '{"time": "2018-01-01T13:50:00-08:00", "status":"Identified", "message":"oh no! i am broken", "Title":"User API is down"}' -H "Content-Type: application/json" -X POST http://localhost/api/incidents ``` + +The token used is configured on the server using the token flag or env variable. + +## CLI + +The statuspage binary contains a CLI tool that can be used to access the API. +For more information run `./statuspage cli --help`. Every api endpoint is +available in this tool. From 881cd57a0af45517e8e266d11a919c934450d594 Mon Sep 17 00:00:00 2001 From: Eirik Martiniussen Sylliaas Date: Sun, 7 Jan 2018 23:28:59 +0100 Subject: [PATCH 4/5] Change logger --- Gopkg.lock | 16 ++++++++++++++-- cmd/server.go | 11 +++++++++-- docker-compose.yaml | 2 ++ pkg/middleware.go | 38 ++++++++++++++++++++++++++++++++++++++ pkg/routes/dashboard.go | 2 +- pkg/utils.go | 2 +- 6 files changed, 65 insertions(+), 6 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 038360c..87e17cf 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -79,6 +79,12 @@ revision = "16398bac157da96aa88f98a2df640c7f32af1da2" version = "v1.0.1" +[[projects]] + name = "github.com/sirupsen/logrus" + packages = ["."] + revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" + version = "v1.0.4" + [[projects]] name = "github.com/spf13/afero" packages = [".","mem"] @@ -121,10 +127,16 @@ packages = ["codec"] revision = "54210f4e076c57f351166f0ed60e67d3fca57a36" +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = ["ssh/terminal"] + revision = "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8" + [[projects]] branch = "master" name = "golang.org/x/sys" - packages = ["unix"] + packages = ["unix","windows"] revision = "314a259e304ff91bd6985da2a7149bbf91237993" [[projects]] @@ -148,6 +160,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "0f5003afd9b899e77a0885d497f248226a971b8a6f837c63b77aee4c4385c965" + inputs-digest = "de9a7002b0063888c213a05fea4e6306ddb1cc0e4775d31c47b2dfc714dcd4ed" solver-name = "gps-cdcl" solver-version = 1 diff --git a/cmd/server.go b/cmd/server.go index 1cbb8ec..2f642cc 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -5,6 +5,7 @@ import ( "github.com/eirsyl/statuspage/pkg/routes" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -47,8 +48,10 @@ var serverCmd = &cobra.Command{ pkg.ConfigRuntime() gin.SetMode(gin.ReleaseMode) - router := gin.Default() + router := gin.New() + router.Use(gin.Recovery()) router.Use(pkg.State()) + router.Use(pkg.Logger()) binding.Validator.RegisterValidation("incidentstatus", pkg.IncidentStatus) binding.Validator.RegisterValidation("servicestatus", pkg.ServiceStatus) @@ -79,6 +82,10 @@ var serverCmd = &cobra.Command{ } listenAddress := viper.GetString("listenAddress") - router.Run(listenAddress) + log.WithFields(log.Fields{"address": listenAddress}).Info("Starting server") + err := router.Run(listenAddress) + if err != nil { + log.Error("Server exited ", err) + } }, } diff --git a/docker-compose.yaml b/docker-compose.yaml index e84d016..ddb698a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,6 +3,8 @@ version: "3" services: postgres: image: postgres:9.5 + ports: + - 5432:5432 volumes: - /var/lib/postgresql/data environment: diff --git a/pkg/middleware.go b/pkg/middleware.go index 882eccb..7f25369 100644 --- a/pkg/middleware.go +++ b/pkg/middleware.go @@ -3,8 +3,10 @@ package pkg import ( "github.com/gin-gonic/gin" "github.com/go-pg/pg" + log "github.com/sirupsen/logrus" "github.com/spf13/viper" "net/http" + "time" ) // Auth enforces token authentication on the api endpoints. @@ -51,3 +53,39 @@ func State() gin.HandlerFunc { c.Next() } } + +// Logger add logrus logging to each request +func Logger() gin.HandlerFunc { + return func(c *gin.Context) { + // Start timer + start := time.Now() + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + + // Process request + c.Next() + + // Stop timer + end := time.Now() + latency := end.Sub(start) + + clientIP := c.ClientIP() + method := c.Request.Method + statusCode := c.Writer.Status() + comment := c.Errors.ByType(gin.ErrorTypePrivate).String() + + if raw != "" { + path = path + "?" + raw + } + + log.WithFields(log.Fields{ + "statusCode": statusCode, + "latency": latency, + "clientIP": clientIP, + "method": method, + "path": path, + "comment": comment, + }).Info("Request") + + } +} diff --git a/pkg/routes/dashboard.go b/pkg/routes/dashboard.go index b1629f9..c2db899 100644 --- a/pkg/routes/dashboard.go +++ b/pkg/routes/dashboard.go @@ -3,8 +3,8 @@ package routes import ( "github.com/eirsyl/statuspage/pkg" "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" "github.com/spf13/viper" - "log" "net/http" ) diff --git a/pkg/utils.go b/pkg/utils.go index daabbbe..3f01dbd 100644 --- a/pkg/utils.go +++ b/pkg/utils.go @@ -3,7 +3,7 @@ package pkg import ( "github.com/go-pg/pg" "github.com/go-pg/pg/orm" - "log" + log "github.com/sirupsen/logrus" "runtime" "time" ) From 75dd522c7925dccb2a1a12d6300223169602807b Mon Sep 17 00:00:00 2001 From: Eirik Martiniussen Sylliaas Date: Mon, 8 Jan 2018 12:33:11 +0100 Subject: [PATCH 5/5] Initial http client structure --- cmd/cli/incident.go | 16 +++++++++ cmd/cli/service.go | 65 +++++++++++++++++++++++++++++++++++++ cmd/{cli.go => cli_root.go} | 9 +++-- docker-compose.yaml | 2 +- pkg/api/client.go | 54 ++++++++++++++++++++++++++++++ pkg/api/services.go | 29 +++++++++++++++++ pkg/middleware.go | 1 + pkg/utils.go | 9 +++++ 8 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 cmd/cli/incident.go create mode 100644 cmd/cli/service.go rename cmd/{cli.go => cli_root.go} (80%) create mode 100644 pkg/api/client.go create mode 100644 pkg/api/services.go diff --git a/cmd/cli/incident.go b/cmd/cli/incident.go new file mode 100644 index 0000000..b04399c --- /dev/null +++ b/cmd/cli/incident.go @@ -0,0 +1,16 @@ +package cli + +import ( + "github.com/spf13/cobra" +) + +func init() { +} + +var IncidentCmd = &cobra.Command{ + Use: "incident", + Short: "Manage incidents and updates", + Run: func(cmd *cobra.Command, args []string) { + + }, +} diff --git a/cmd/cli/service.go b/cmd/cli/service.go new file mode 100644 index 0000000..723e614 --- /dev/null +++ b/cmd/cli/service.go @@ -0,0 +1,65 @@ +package cli + +import ( + "github.com/eirsyl/statuspage/pkg/api" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + log "github.com/sirupsen/logrus" +) + +func init() { + ServiceCmd.AddCommand(listServiceCmd) + ServiceCmd.AddCommand(createServiceCmd) + ServiceCmd.AddCommand(updateServiceCmd) + ServiceCmd.AddCommand(deleteServiceCmd) + ServiceCmd.AddCommand(getServiceCmd) +} + +var ServiceCmd = &cobra.Command{ + Use: "service", + Short: "Manage services", +} + +var listServiceCmd = &cobra.Command{ + Use: "list", + Run: func(cmd *cobra.Command, args []string) { + apiUrl, token := viper.GetString("apiUrl"), viper.GetString("token") + api, err := api.NewAPI(apiUrl, token) + if err != nil { + log.Fatal(err) + } + services, err := api.ListServices() + log.Info(services, err) + }, +} + +var createServiceCmd = &cobra.Command{ + Use: "create", + Run: func(cmd *cobra.Command, args []string) { + + }, +} + +var updateServiceCmd = &cobra.Command{ + Use: "update [id]", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + + }, +} + +var deleteServiceCmd = &cobra.Command{ + Use: "delete [id]", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + + }, +} + +var getServiceCmd = &cobra.Command{ + Use: "get [id]", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + }, +} diff --git a/cmd/cli.go b/cmd/cli_root.go similarity index 80% rename from cmd/cli.go rename to cmd/cli_root.go index 1198578..cab1dcc 100644 --- a/cmd/cli.go +++ b/cmd/cli_root.go @@ -1,9 +1,10 @@ package cmd import ( - "fmt" "github.com/spf13/cobra" "github.com/spf13/viper" + + "github.com/eirsyl/statuspage/cmd/cli" ) func init() { @@ -13,14 +14,12 @@ func init() { viper.BindPFlag("token", cliCmd.PersistentFlags().Lookup("token")) viper.BindEnv("apiUrl", "API_URL") viper.BindEnv("token", "TOKEN") + cliCmd.AddCommand(cli.IncidentCmd) + cliCmd.AddCommand(cli.ServiceCmd) RootCmd.AddCommand(cliCmd) } var cliCmd = &cobra.Command{ Use: "cli", Short: "Access the statuspage api from the command line", - Run: func(cmd *cobra.Command, args []string) { - apiUrl, token := viper.GetString("apiUrl"), viper.GetString("token") - fmt.Print(apiUrl, token) - }, } diff --git a/docker-compose.yaml b/docker-compose.yaml index ddb698a..c91f28a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -19,7 +19,7 @@ services: links: - postgres:postgres environment: - - API_TOKEN= # This is the token clients should use to access the API (AUTHORIZATION header) + - TOKEN=auth # This is the token clients should use to access the API (AUTHORIZATION header) - POSTGRES_ADDRESS=postgres:5432 # The address of the postgres instance - POSTGRES_USER=statuspage # The postgres username for authorization - POSTGRES_PASSWORD=statuspage # The postgres password for authorization diff --git a/pkg/api/client.go b/pkg/api/client.go new file mode 100644 index 0000000..043bbe2 --- /dev/null +++ b/pkg/api/client.go @@ -0,0 +1,54 @@ +package api + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/eirsyl/statuspage/pkg" + "net/http" + "time" +) + +// NewClient returns a http.Client used to access the API +func NewClient() *http.Client { + return &http.Client{ + Timeout: 10 * time.Second, + } +} + +type API struct { + apiUrl string + token string + client *http.Client +} + +// NewAPI initializes a new API client that can be used to access the API +func NewAPI(apiUrl, token string) (*API, error) { + client := NewClient() + + if apiUrl == "" || token == "" { + return nil, errors.New("Both apiUrl and token is required") + } + + return &API{ + apiUrl: apiUrl, + token: token, + client: client, + }, nil +} + +func (a *API) CreateRequest(url, method string, payload interface{}) *http.Request { + var data []byte + + if payload != nil { + data, _ = json.Marshal(payload) + } + + request, _ := http.NewRequest( + method, fmt.Sprintf("%s%s", pkg.RemoveLastSlash(a.apiUrl), url), bytes.NewReader(data), + ) + request.Header.Add("AUTHORIZATION", a.token) + request.Header.Add("CONTENT-TYPE", "application/json") + return request +} diff --git a/pkg/api/services.go b/pkg/api/services.go new file mode 100644 index 0000000..a87db1c --- /dev/null +++ b/pkg/api/services.go @@ -0,0 +1,29 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/eirsyl/statuspage/pkg" + "io/ioutil" +) + +func (a *API) ListServices() ([]pkg.Service, error) { + request := a.CreateRequest("/api/services", "GET", nil) + response, err := a.client.Do(request) + if err == nil && response.StatusCode == 200 { + var result []pkg.Service + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return []pkg.Service{}, err + } + json.Unmarshal(body, &result) + return result, nil + } + + if err == nil && response.StatusCode != 200 { + return []pkg.Service{}, errors.New(fmt.Sprintf("Response failed %s", response.StatusCode)) + } + + return []pkg.Service{}, err +} diff --git a/pkg/middleware.go b/pkg/middleware.go index 7f25369..bb9cc7c 100644 --- a/pkg/middleware.go +++ b/pkg/middleware.go @@ -17,6 +17,7 @@ func Auth() gin.HandlerFunc { token := c.GetHeader("Authorization") if validToken == "" || token != validToken { + log.WithFields(log.Fields{"token": token}).Warn("Permission denied, invalid token") c.AbortWithStatus(http.StatusUnauthorized) return } diff --git a/pkg/utils.go b/pkg/utils.go index 3f01dbd..8be0e4f 100644 --- a/pkg/utils.go +++ b/pkg/utils.go @@ -116,3 +116,12 @@ func CreateSchema(db *pg.DB) error { } return nil } + +// RemoveLastSlash removes the last character in a string if the character is a string. +func RemoveLastSlash(url string) string { + l := len(url) + if l > 0 && url[l-1] == '/' { + return url[:l-2] + } + return url +}