diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 4c31afa..f86cf30 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -3,6 +3,7 @@ package bootstrap import ( "context" + "github.com/JulesMike/spoty/build" "github.com/JulesMike/spoty/cache" "github.com/JulesMike/spoty/config" "github.com/JulesMike/spoty/http" @@ -12,6 +13,7 @@ import ( // Module exported for initialising application. var Module = fx.Options( + build.Module, config.Module, cache.Module, http.Module, diff --git a/build/build.go b/build/build.go new file mode 100644 index 0000000..3720d5d --- /dev/null +++ b/build/build.go @@ -0,0 +1,56 @@ +package build + +import ( + "fmt" + "log" + "runtime/debug" + "time" + + "go.uber.org/fx" +) + +// Module exported for initialising a new build Info. +var Module = fx.Options( + fx.Provide(New), +) + +// Info contains the information about the build. +type Info struct { + Revision string `json:"revision"` + LastCommit time.Time `json:"last_commit"` + DirtyBuild bool `json:"dirty_build"` +} + +// New returns a new instance of Info. +func New() (*Info, error) { + bi, ok := debug.ReadBuildInfo() + if !ok { + return nil, fmt.Errorf("failed to read build info") + } + + info := Info{ + Revision: "n/a", + LastCommit: time.Time{}, + DirtyBuild: false, + } + + for i := range bi.Settings { + kv := &bi.Settings[i] + + switch kv.Key { + case "vcs.revision": + info.Revision = kv.Value + case "vcs.time": + hash, err := time.Parse(time.RFC3339, kv.Value) + if err != nil { + log.Printf("failed to parse vcs.time: %v", err) + } + + info.LastCommit = hash + case "vcs.modified": + info.DirtyBuild = kv.Value == "true" + } + } + + return &info, nil +} diff --git a/docs/docs.go b/docs/docs.go index 66e0c5e..c75fbf4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -22,7 +22,7 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/api": { + "/": { "get": { "description": "checks if server is running", "produces": [ @@ -36,7 +36,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/server.Success" + "$ref": "#/definitions/http.Success" } } } @@ -62,7 +62,7 @@ const docTemplate = `{ "403": { "description": "already authenticated", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } } } @@ -98,19 +98,19 @@ const docTemplate = `{ "200": { "description": "authenticated successfully", "schema": { - "$ref": "#/definitions/server.Success" + "$ref": "#/definitions/http.Success" } }, "403": { "description": "could not retrieve token", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } }, "404": { "description": "could not retrieve current user", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } } } @@ -136,13 +136,13 @@ const docTemplate = `{ "401": { "description": "not authenticated", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } }, "404": { "description": "no current playing track found", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } } } @@ -171,19 +171,39 @@ const docTemplate = `{ "401": { "description": "not authenticated", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } }, "404": { "description": "no current playing track found", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } }, "500": { "description": "album images could not be processed", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" + } + } + } + } + }, + "/api/version": { + "get": { + "description": "checks the server's version", + "produces": [ + "application/json" + ], + "tags": [ + "core" + ], + "summary": "Health Check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/build.Info" } } } @@ -191,6 +211,20 @@ const docTemplate = `{ } }, "definitions": { + "build.Info": { + "type": "object", + "properties": { + "dirty_build": { + "type": "boolean" + }, + "last_commit": { + "type": "string" + }, + "revision": { + "type": "string" + } + } + }, "color.RGBA": { "type": "object", "properties": { @@ -199,7 +233,7 @@ const docTemplate = `{ } } }, - "server.Error": { + "http.Error": { "type": "object", "properties": { "error": { @@ -207,7 +241,7 @@ const docTemplate = `{ } } }, - "server.Success": { + "http.Success": { "type": "object", "properties": { "success": { diff --git a/docs/swagger.json b/docs/swagger.json index 8857ee3..bfd4f62 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -13,7 +13,7 @@ "version": "v0.2.0" }, "paths": { - "/api": { + "/": { "get": { "description": "checks if server is running", "produces": [ @@ -27,7 +27,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/server.Success" + "$ref": "#/definitions/http.Success" } } } @@ -53,7 +53,7 @@ "403": { "description": "already authenticated", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } } } @@ -89,19 +89,19 @@ "200": { "description": "authenticated successfully", "schema": { - "$ref": "#/definitions/server.Success" + "$ref": "#/definitions/http.Success" } }, "403": { "description": "could not retrieve token", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } }, "404": { "description": "could not retrieve current user", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } } } @@ -127,13 +127,13 @@ "401": { "description": "not authenticated", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } }, "404": { "description": "no current playing track found", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } } } @@ -162,19 +162,39 @@ "401": { "description": "not authenticated", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } }, "404": { "description": "no current playing track found", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" } }, "500": { "description": "album images could not be processed", "schema": { - "$ref": "#/definitions/server.Error" + "$ref": "#/definitions/http.Error" + } + } + } + } + }, + "/api/version": { + "get": { + "description": "checks the server's version", + "produces": [ + "application/json" + ], + "tags": [ + "core" + ], + "summary": "Health Check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/build.Info" } } } @@ -182,6 +202,20 @@ } }, "definitions": { + "build.Info": { + "type": "object", + "properties": { + "dirty_build": { + "type": "boolean" + }, + "last_commit": { + "type": "string" + }, + "revision": { + "type": "string" + } + } + }, "color.RGBA": { "type": "object", "properties": { @@ -190,7 +224,7 @@ } } }, - "server.Error": { + "http.Error": { "type": "object", "properties": { "error": { @@ -198,7 +232,7 @@ } } }, - "server.Success": { + "http.Success": { "type": "object", "properties": { "success": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 960c712..c318cac 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,15 +1,24 @@ definitions: + build.Info: + properties: + dirty_build: + type: boolean + last_commit: + type: string + revision: + type: string + type: object color.RGBA: properties: r: type: integer type: object - server.Error: + http.Error: properties: error: type: string type: object - server.Success: + http.Success: properties: success: type: string @@ -236,7 +245,7 @@ info: title: Spoty API version: v0.2.0 paths: - /api: + /: get: description: checks if server is running produces: @@ -245,7 +254,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/server.Success' + $ref: '#/definitions/http.Success' summary: Health Check tags: - core @@ -262,7 +271,7 @@ paths: "403": description: already authenticated schema: - $ref: '#/definitions/server.Error' + $ref: '#/definitions/http.Error' summary: Authentication tags: - spoty @@ -286,15 +295,15 @@ paths: "200": description: authenticated successfully schema: - $ref: '#/definitions/server.Success' + $ref: '#/definitions/http.Success' "403": description: could not retrieve token schema: - $ref: '#/definitions/server.Error' + $ref: '#/definitions/http.Error' "404": description: could not retrieve current user schema: - $ref: '#/definitions/server.Error' + $ref: '#/definitions/http.Error' summary: Callback tags: - spoty @@ -311,11 +320,11 @@ paths: "401": description: not authenticated schema: - $ref: '#/definitions/server.Error' + $ref: '#/definitions/http.Error' "404": description: no current playing track found schema: - $ref: '#/definitions/server.Error' + $ref: '#/definitions/http.Error' summary: Current Playing Track tags: - spoty @@ -334,16 +343,29 @@ paths: "401": description: not authenticated schema: - $ref: '#/definitions/server.Error' + $ref: '#/definitions/http.Error' "404": description: no current playing track found schema: - $ref: '#/definitions/server.Error' + $ref: '#/definitions/http.Error' "500": description: album images could not be processed schema: - $ref: '#/definitions/server.Error' + $ref: '#/definitions/http.Error' summary: Album Images of Current Playing Track tags: - spoty + /api/version: + get: + description: checks the server's version + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/build.Info' + summary: Health Check + tags: + - core swagger: "2.0" diff --git a/http/route.go b/http/handler.go similarity index 75% rename from http/route.go rename to http/handler.go index a0603b9..0c9537e 100644 --- a/http/route.go +++ b/http/handler.go @@ -24,14 +24,27 @@ type Error struct { // @Description checks if server is running // @Tags core // @Produce json -// @Success 200 {object} server.Success -// @Router /api [get] +// @Success 200 {object} http.Success +// @Router / [get] func (Server) handleHealthCheck() gin.HandlerFunc { return func(c *gin.Context) { c.JSON(http.StatusOK, Success{Success: "i'm alright!"}) } } +// handleVersion godoc +// @Summary Health Check +// @Description checks the server's version +// @Tags core +// @Produce json +// @Success 200 {object} build.Info +// @Router /api/version [get] +func (s *Server) handleVersion() gin.HandlerFunc { + return func(c *gin.Context) { + c.JSON(http.StatusOK, s.build) + } +} + func (s *Server) handleSwagger() gin.HandlerFunc { docs.SwaggerInfo.Host = s.addr docs.SwaggerInfo.BasePath = "/" @@ -47,8 +60,8 @@ func (s *Server) handleSwagger() gin.HandlerFunc { // @Tags spoty // @Produce json // @Success 200 {object} spotify.FullTrack "returns full track information" -// @Failure 401 {object} server.Error "not authenticated" -// @Failure 404 {object} server.Error "no current playing track found" +// @Failure 401 {object} http.Error "not authenticated" +// @Failure 404 {object} http.Error "no current playing track found" // @Router /api/current [get] func (s *Server) handleCurrentTrack(c *gin.Context) { track, err := s.spoty.TrackCurrentlyPlaying() @@ -67,9 +80,9 @@ func (s *Server) handleCurrentTrack(c *gin.Context) { // @Tags spoty // @Produce json // @Success 200 {array} spoty.Image "returns album images" -// @Failure 401 {object} server.Error "not authenticated" -// @Failure 404 {object} server.Error "no current playing track found" -// @Failure 500 {object} server.Error "album images could not be processed" +// @Failure 401 {object} http.Error "not authenticated" +// @Failure 404 {object} http.Error "no current playing track found" +// @Failure 500 {object} http.Error "album images could not be processed" // @Router /api/current/images [get] func (s *Server) handleCurrentTrackImages(c *gin.Context) { track, err := s.spoty.TrackCurrentlyPlaying() @@ -98,7 +111,7 @@ func (s *Server) handleCurrentTrackImages(c *gin.Context) { // @Tags spoty // @Produce json // @Success 302 {string} string "redirection to spotify" -// @Failure 403 {object} server.Error "already authenticated" +// @Failure 403 {object} http.Error "already authenticated" // @Router /api/authenticate [get] func (s *Server) handleAuthenticate(c *gin.Context) { c.Redirect(http.StatusFound, s.spoty.AuthURL()) @@ -111,10 +124,10 @@ func (s *Server) handleAuthenticate(c *gin.Context) { // @Produce json // @Param code query string true "code from spotify" // @Param state query string true "state from spotify" -// @Success 200 {object} server.Success "authenticated successfully" -// @Failure 403 {object} server.Error "already authenticated" -// @Failure 403 {object} server.Error "could not retrieve token" -// @Failure 404 {object} server.Error "could not retrieve current user" +// @Success 200 {object} http.Success "authenticated successfully" +// @Failure 403 {object} http.Error "already authenticated" +// @Failure 403 {object} http.Error "could not retrieve token" +// @Failure 404 {object} http.Error "could not retrieve current user" // @Router /api/callback [get] func (s *Server) handleCallback(c *gin.Context) { if err := s.spoty.SetupNewClient(c.Request); err != nil { diff --git a/http/server.go b/http/server.go index 4101bd9..718176b 100644 --- a/http/server.go +++ b/http/server.go @@ -7,6 +7,7 @@ import ( "net/http" "time" + "github.com/JulesMike/spoty/build" "github.com/JulesMike/spoty/config" "github.com/JulesMike/spoty/spoty" "github.com/gin-gonic/gin" @@ -30,11 +31,12 @@ type Server struct { router *gin.Engine http *http.Server spoty *spoty.Spoty + build *build.Info addr string } // New creates a new Server. -func New(cfg *config.Config, spoty *spoty.Spoty) *Server { +func New(cfg *config.Config, spoty *spoty.Spoty, build *build.Info) *Server { if cfg.Prod { gin.SetMode(gin.ReleaseMode) } @@ -43,6 +45,7 @@ func New(cfg *config.Config, spoty *spoty.Spoty) *Server { router: gin.Default(), addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), spoty: spoty, + build: build, } s.http = &http.Server{ @@ -59,13 +62,16 @@ func New(cfg *config.Config, spoty *spoty.Spoty) *Server { // RegisterRoutes registers the REST HTTP routes. func (s *Server) RegisterRoutes() { + // Health Check + s.router.GET("/", s.handleHealthCheck()) + // Swagger s.router.GET("/swagger/*any", s.handleSwagger()) api := s.router.Group("/api") { - // Health Check - api.GET("/", s.handleHealthCheck()) + // Version + api.GET("/version", s.handleVersion()) // Guest routes guest := api.Group("/")