From e1c4c58684841219e92c38f47c76b7fa2d5032d3 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Sat, 18 Dec 2021 00:37:52 +0100 Subject: [PATCH 1/2] add swagger page --- .pre-commit-config.yaml | 8 ------- Makefile | 6 +++-- src/model/rauc_status.go | 6 ++--- src/server/server.go | 4 +++- src/server/{ => swagger}/docs.go | 8 +------ src/server/swagger/swagger.go | 24 +++++++++++++++++++ src/server/swagger/swagger.html | 20 ++++++++++++++++ .../server/swagger/swagger.yaml | 11 +++------ 8 files changed, 57 insertions(+), 30 deletions(-) rename src/server/{ => swagger}/docs.go (61%) create mode 100644 src/server/swagger/swagger.go create mode 100644 src/server/swagger/swagger.html rename swagger.yaml => src/server/swagger/swagger.yaml (95%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c597da4..cc4ae18 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,14 +9,6 @@ repos: - repo: local hooks: - - id: check-swagger - name: check-swagger - language: system - files: swagger.yaml - entry: swagger - args: ["validate", "swagger.yaml"] - pass_filenames: false - - id: tsc name: tsc entry: tsc diff --git a/Makefile b/Makefile index 36c51f6..2a1cbee 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ SRC_DIR=./src UI_DIR=./ui +APIDOC_FILE=./src/server/swagger/swagger.yaml + VERSION=$(shell git tag --sort=-version:refname | head -n 1) setup: @@ -22,10 +24,10 @@ build-server: build: build-ui build-server generate-apidoc: - SWAGGER_GENERATE_EXTENSION=false swagger generate spec --scan-models -o swagger.yaml + SWAGGER_GENERATE_EXTENSION=false swagger generate spec --scan-models -o ${APIDOC_FILE} generate-apiclient: - openapi-generator generate -i swagger.yaml -g typescript-axios -o ${UI_DIR}/src/sebrauc-client -p "supportsES6=true" + openapi-generator generate -i ${APIDOC_FILE} -g typescript-axios -o ${UI_DIR}/src/sebrauc-client -p "supportsES6=true" cd ${UI_DIR} && npm run format clean: diff --git a/src/model/rauc_status.go b/src/model/rauc_status.go index 52eb88b..d71d64d 100644 --- a/src/model/rauc_status.go +++ b/src/model/rauc_status.go @@ -19,13 +19,11 @@ type RaucStatus struct { // Installation error message // required: true - //nolint:lll - // example: Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle? + // example: Failed to check bundle identifier: Invalid identifier. LastError string `json:"last_error"` // Full command line output of the current installation // required: true - //nolint:lll - // example: 0% Installing\n0% Determining slot states\n20% Determining slot states done\n + // example: 0% Installing 0% Determining slot states 20% Determining slot states done Log string `json:"log"` } diff --git a/src/server/server.go b/src/server/server.go index 7452edd..3f91ecd 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -10,6 +10,7 @@ import ( "code.thetadev.de/TSGRain/SEBRAUC/src/rauc" "code.thetadev.de/TSGRain/SEBRAUC/src/server/mode" "code.thetadev.de/TSGRain/SEBRAUC/src/server/stream" + "code.thetadev.de/TSGRain/SEBRAUC/src/server/swagger" "code.thetadev.de/TSGRain/SEBRAUC/src/sysinfo" "code.thetadev.de/TSGRain/SEBRAUC/src/util" "code.thetadev.de/TSGRain/SEBRAUC/ui" @@ -58,9 +59,10 @@ func (srv *SEBRAUCServer) Run() error { router.POST("/api/update", srv.controllerUpdate) router.POST("/api/reboot", srv.controllerReboot) - // router.StaticFS("/", ui.GetFS()) ui.Register(router) + swagger.Register(router) + return router.Run(srv.address) } diff --git a/src/server/docs.go b/src/server/swagger/docs.go similarity index 61% rename from src/server/docs.go rename to src/server/swagger/docs.go index 1704805..198db73 100644 --- a/src/server/docs.go +++ b/src/server/swagger/docs.go @@ -7,11 +7,5 @@ // Version: 0.2.0 // License: MIT // -// Consumes: -// - application/json -// -// Produces: -// - application/json -// // swagger:meta -package server +package swagger diff --git a/src/server/swagger/swagger.go b/src/server/swagger/swagger.go new file mode 100644 index 0000000..732f009 --- /dev/null +++ b/src/server/swagger/swagger.go @@ -0,0 +1,24 @@ +package swagger + +import ( + _ "embed" + + "github.com/gin-gonic/gin" +) + +//go:embed swagger.html +var swaggerHtml []byte + +//go:embed swagger.yaml +var swaggerYaml []byte + +func Register(r *gin.Engine) { + swg := r.Group("/api/swagger") + + swg.GET("/", func(c *gin.Context) { + c.Data(200, "text/html", swaggerHtml) + }) + swg.GET("/swagger.yaml", func(c *gin.Context) { + c.Data(200, "text/yaml", swaggerYaml) + }) +} diff --git a/src/server/swagger/swagger.html b/src/server/swagger/swagger.html new file mode 100644 index 0000000..cc6855f --- /dev/null +++ b/src/server/swagger/swagger.html @@ -0,0 +1,20 @@ + + + + SEBRAUC API documentation + + + + + + + + + + + diff --git a/swagger.yaml b/src/server/swagger/swagger.yaml similarity index 95% rename from swagger.yaml rename to src/server/swagger/swagger.yaml index 14afa86..24989f5 100644 --- a/swagger.yaml +++ b/src/server/swagger/swagger.yaml @@ -1,5 +1,3 @@ -consumes: - - application/json definitions: RaucStatus: properties: @@ -8,13 +6,12 @@ definitions: type: boolean last_error: description: Installation error message - example: "Failed to check bundle identifier: Invalid identifier. Did you pass - a valid RAUC bundle?" + example: "Failed to check bundle identifier: Invalid identifier." type: string log: description: Full command line output of the current installation - example: 0% Installing\n0% Determining slot states\n20% Determining slot states - done\n + example: 0% Installing 0% Determining slot states 20% Determining slot states + done type: string message: description: Current installation step @@ -193,8 +190,6 @@ paths: description: Server Error schema: $ref: "#/definitions/StatusMessage" -produces: - - application/json schemes: - http - https From 71764dd6fa43541b946dbd662878a780064e0185 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Sun, 19 Dec 2021 17:01:15 +0100 Subject: [PATCH 2/2] add caching/error handling middleware --- go.mod | 2 + go.sum | 4 + src/assets/assets.go | 8 - src/assets/files/index.html | 88 --------- src/main.go | 12 +- src/model/error.go | 24 +++ src/model/rauc_status.go | 4 + src/model/status_message.go | 4 + src/model/system_info.go | 4 + src/rauc/rauc.go | 15 +- src/server/middleware/cache.go | 7 + src/server/middleware/error_handler.go | 58 ++++++ src/server/server.go | 260 ++++++++++++++----------- src/server/swagger/docs.go | 11 -- src/server/swagger/swagger.go | 3 +- src/server/swagger/swagger.yaml | 40 +++- src/util/commands.go | 4 +- src/util/commands_mock.go | 4 +- src/util/errors.go | 6 +- src/util/http_error.go | 39 ++++ src/util/types.go | 5 + ui/src/sebrauc-client/api.ts | 39 +++- ui/ui.go | 3 +- 23 files changed, 388 insertions(+), 256 deletions(-) delete mode 100644 src/assets/assets.go delete mode 100644 src/assets/files/index.html create mode 100644 src/model/error.go create mode 100644 src/server/middleware/cache.go create mode 100644 src/server/middleware/error_handler.go delete mode 100644 src/server/swagger/docs.go create mode 100644 src/util/http_error.go create mode 100644 src/util/types.go diff --git a/go.mod b/go.mod index 36fe1e4..2090ee2 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,11 @@ go 1.16 require ( code.thetadev.de/TSGRain/ginzip v0.1.1 + github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db github.com/fortytw2/leaktest v1.3.0 github.com/gin-contrib/cors v1.3.1 github.com/gin-gonic/gin v1.7.7 + github.com/go-errors/errors v1.4.1 // indirect github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.4.2 github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index c2354c4..7011440 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db h1:oZ4U9IqO8NS+61OmGTBi8vopzqTRxwQeogyBHdrhjbc= +github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db/go.mod h1:Pk7/9x6tyChFTkahDvLBQMlvdsWvfC+yU8HTT5VD314= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= @@ -14,6 +16,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/go-errors/errors v1.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg= +github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= diff --git a/src/assets/assets.go b/src/assets/assets.go deleted file mode 100644 index 8f2c9de..0000000 --- a/src/assets/assets.go +++ /dev/null @@ -1,8 +0,0 @@ -package assets - -import ( - "embed" -) - -//go:embed files/** -var Assets embed.FS diff --git a/src/assets/files/index.html b/src/assets/files/index.html deleted file mode 100644 index 0554e60..0000000 --- a/src/assets/files/index.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - Chat Example - - - - -
- -
- - -
- - diff --git a/src/main.go b/src/main.go index 2e83122..0bbf0f0 100644 --- a/src/main.go +++ b/src/main.go @@ -3,20 +3,28 @@ package main import ( "fmt" "log" + "time" + "code.thetadev.de/TSGRain/SEBRAUC/src/rauc" "code.thetadev.de/TSGRain/SEBRAUC/src/server" + "code.thetadev.de/TSGRain/SEBRAUC/src/server/mode" + "code.thetadev.de/TSGRain/SEBRAUC/src/server/stream" "code.thetadev.de/TSGRain/SEBRAUC/src/util" ) func main() { fmt.Println("SEBRAUC " + util.Version()) + mode.Set(util.Mode) - if util.TestMode { + if mode.IsDev() { fmt.Println("Test mode active - no update operations are executed.") fmt.Println("Build with -tags prod to enable live mode.") } - srv := server.NewServer(":8080") + streamer := stream.New(10*time.Second, 1*time.Second, []string{}) + raucUpdater := &rauc.Rauc{} + + srv := server.NewServer(":8080", streamer, raucUpdater) err := srv.Run() if err != nil { log.Fatalln(err) diff --git a/src/model/error.go b/src/model/error.go new file mode 100644 index 0000000..b2f1396 --- /dev/null +++ b/src/model/error.go @@ -0,0 +1,24 @@ +package model + +// Error model +// +// The Error contains error relevant information. +// +//swagger:model Error +type Error struct { + // The general error message according to HTTP specification. + // + // required: true + // example: Unauthorized + Error string `json:"error"` + // The http error code. + // + // required: true + // example: 500 + StatusCode int `json:"status_code"` + // Concrete error message. + // + // required: true + // example: already running + Message string `json:"msg"` +} diff --git a/src/model/rauc_status.go b/src/model/rauc_status.go index d71d64d..3a48b53 100644 --- a/src/model/rauc_status.go +++ b/src/model/rauc_status.go @@ -1,5 +1,9 @@ package model +// RaucStatus model +// +// RaucStatus contains information about the current RAUC updater status. +// //swagger:model RaucStatus type RaucStatus struct { // True if the installer is running diff --git a/src/model/status_message.go b/src/model/status_message.go index 256b004..d01fe03 100644 --- a/src/model/status_message.go +++ b/src/model/status_message.go @@ -1,5 +1,9 @@ package model +// StatusMessage model +// +// StatusMessage contains the status of an operation. +// //swagger:model StatusMessage type StatusMessage struct { // Is operation successful? diff --git a/src/model/system_info.go b/src/model/system_info.go index ec02bbb..6c923d7 100644 --- a/src/model/system_info.go +++ b/src/model/system_info.go @@ -1,5 +1,9 @@ package model +// SystemInfo model +// +// SystemInfo contains information about the running system. +// //swagger:model SystemInfo type SystemInfo struct { // Hostname of the system diff --git a/src/rauc/rauc.go b/src/rauc/rauc.go index 5c1b4be..e84b142 100644 --- a/src/rauc/rauc.go +++ b/src/rauc/rauc.go @@ -19,23 +19,14 @@ var ( ) type Rauc struct { - bc broadcaster + bc util.Broadcaster status model.RaucStatus runningMtx sync.Mutex } -type broadcaster interface { - Broadcast(msg []byte) -} - -func NewRauc(bc broadcaster) *Rauc { - r := &Rauc{ - bc: bc, - } - +func (r *Rauc) SetBroadcaster(bc util.Broadcaster) { + r.bc = bc r.bc.Broadcast(r.GetStatusJson()) - - return r } func (r *Rauc) completed(updateFile string) { diff --git a/src/server/middleware/cache.go b/src/server/middleware/cache.go new file mode 100644 index 0000000..8c97b26 --- /dev/null +++ b/src/server/middleware/cache.go @@ -0,0 +1,7 @@ +package middleware + +import "github.com/gin-gonic/gin" + +func Cache(c *gin.Context) { + c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable") +} diff --git a/src/server/middleware/error_handler.go b/src/server/middleware/error_handler.go new file mode 100644 index 0000000..e774c66 --- /dev/null +++ b/src/server/middleware/error_handler.go @@ -0,0 +1,58 @@ +package middleware + +import ( + "errors" + "fmt" + "net/http" + + "code.thetadev.de/TSGRain/SEBRAUC/src/model" + "code.thetadev.de/TSGRain/SEBRAUC/src/util" + nice "github.com/ekyoung/gin-nice-recovery" + "github.com/gin-gonic/gin" +) + +// ErrorHandler creates a gin middleware for handling errors. +func ErrorHandler(isApi bool) gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + + if len(c.Errors) > 0 { + for _, e := range c.Errors { + writeError(c, e.Err, isApi) + } + } + } +} + +func PanicHandler(isApi bool) gin.HandlerFunc { + return nice.Recovery(func(c *gin.Context, err interface{}) { + writeError(c, fmt.Errorf("panic: %s", err), isApi) + }) +} + +func writeError(c *gin.Context, err error, isApi bool) { + status := http.StatusInternalServerError + + var httpErr util.HttpError + if errors.As(err, &httpErr) { + status = httpErr.StatusCode() + } + + // only write error message if there is no content + if c.Writer.Size() != -1 { + c.Status(status) + return + } + + if isApi { + // Machine-readable JSON error message + c.JSON(status, &model.Error{ + Error: http.StatusText(status), + StatusCode: status, + Message: err.Error(), + }) + } else { + // Human-readable error message + c.String(status, "%d %s: %s", status, http.StatusText(status), err.Error()) + } +} diff --git a/src/server/server.go b/src/server/server.go index 3f91ecd..3f1eafb 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -1,15 +1,24 @@ +// SEBRAUC +// +// REST API for the SEBRAUC firmware updater +// +// --- +// Schemes: http, https +// Version: 0.2.0 +// License: MIT +// +// swagger:meta package server import ( "errors" "fmt" - "strings" + "net/http" "time" "code.thetadev.de/TSGRain/SEBRAUC/src/model" - "code.thetadev.de/TSGRain/SEBRAUC/src/rauc" + "code.thetadev.de/TSGRain/SEBRAUC/src/server/middleware" "code.thetadev.de/TSGRain/SEBRAUC/src/server/mode" - "code.thetadev.de/TSGRain/SEBRAUC/src/server/stream" "code.thetadev.de/TSGRain/SEBRAUC/src/server/swagger" "code.thetadev.de/TSGRain/SEBRAUC/src/sysinfo" "code.thetadev.de/TSGRain/SEBRAUC/src/util" @@ -19,17 +28,28 @@ import ( "github.com/google/uuid" ) -type SEBRAUCServer struct { - address string - raucUpdater *rauc.Rauc - streamer *stream.API - tmpdir string +type serverStreamer interface { + util.Broadcaster + Handle(ctx *gin.Context) + Close() } -func NewServer(address string) *SEBRAUCServer { - streamer := stream.New(10*time.Second, 1*time.Second, []string{}) +type serverUpdater interface { + GetStatus() model.RaucStatus + RunRauc(updateFile string) error + SetBroadcaster(bc util.Broadcaster) +} - raucUpdater := rauc.NewRauc(streamer) +type SEBRAUCServer struct { + address string + streamer serverStreamer + udater serverUpdater + tmpdir string +} + +func NewServer(address string, streamer serverStreamer, + updater serverUpdater) *SEBRAUCServer { + updater.SetBroadcaster(streamer) tmpdir, err := util.GetTmpdir() if err != nil { @@ -37,27 +57,44 @@ func NewServer(address string) *SEBRAUCServer { } return &SEBRAUCServer{ - address: address, - raucUpdater: raucUpdater, - streamer: streamer, - tmpdir: tmpdir, + address: address, + udater: updater, + streamer: streamer, + tmpdir: tmpdir, } } func (srv *SEBRAUCServer) Run() error { - router := gin.Default() + router := gin.New() + router.Use(gin.Logger()) + _ = router.SetTrustedProxies(nil) if mode.IsDev() { router.Use(cors.Default()) } - // ROUTES - router.GET("/api/ws", srv.streamer.Handle) - router.GET("/api/status", srv.controllerStatus) - router.GET("/api/info", srv.controllerInfo) + router.Use(middleware.ErrorHandler(false), middleware.PanicHandler(false)) + router.NoRoute(func(c *gin.Context) { c.Error(util.ErrPageNotFound) }) - router.POST("/api/update", srv.controllerUpdate) - router.POST("/api/reboot", srv.controllerReboot) + api := router.Group("/api", + middleware.ErrorHandler(true), middleware.PanicHandler(true)) + + // ROUTES + api.GET("/ws", srv.streamer.Handle) + api.GET("/status", srv.controllerStatus) + api.GET("/info", srv.controllerInfo) + + api.POST("/update", srv.controllerUpdate) + api.POST("/reboot", srv.controllerReboot) + + // Error routes for testing + if mode.IsDev() { + router.GET("/error", srv.controllerError) + router.GET("/panic", srv.controllerPanic) + + api.GET("/error", srv.controllerError) + api.GET("/panic", srv.controllerPanic) + } ui.Register(router) @@ -66,40 +103,34 @@ func (srv *SEBRAUCServer) Run() error { return router.Run(srv.address) } -// @Description Start the update process -// @Route /update +// swagger:operation POST /update startUpdate // -// _Param {name} {in} {goType} {required} {description} -// @Param updateFile form file true "Rauc firmware image file (*.raucb)" +// Start the update process // -// _Success {status} {jsonType} {goType} {description} -// @Success 200 object statusMessage -// @Failure 500 object statusMessage "Server error" +// --- +// consumes: +// - multipart/form-data +// produces: [application/json] +// parameters: +// - name: updateFile +// in: formData +// description: RAUC firmware image file (*.raucb) +// required: true +// type: file +// responses: +// 200: +// description: Ok +// schema: +// $ref: "#/definitions/StatusMessage" +// 409: +// description: already running +// schema: +// $ref: "#/definitions/Error" +// 500: +// description: Server Error +// schema: +// $ref: "#/definitions/Error" func (srv *SEBRAUCServer) controllerUpdate(c *gin.Context) { - // swagger:operation POST /update startUpdate - // - // Start the update process - // - // --- - // consumes: - // - multipart/form-data - // produces: [application/json] - // parameters: - // - name: updateFile - // in: formData - // description: Rauc firmware image file (*.raucb) - // required: true - // type: file - // responses: - // 200: - // description: Ok - // schema: - // $ref: "#/definitions/StatusMessage" - // 500: - // description: Server Error - // schema: - // $ref: "#/definitions/StatusMessage" - file, err := c.FormFile("updateFile") if err != nil { c.Error(err) @@ -120,100 +151,93 @@ func (srv *SEBRAUCServer) controllerUpdate(c *gin.Context) { return } - err = srv.raucUpdater.RunRauc(updateFile) + err = srv.udater.RunRauc(updateFile) if err == nil { writeStatus(c, true, "Update started") } else if errors.Is(err, util.ErrAlreadyRunning) { - c.AbortWithError(409, errors.New("already running")) + writeStatus(c, false, "Updater already running") } else { c.Error(err) return } } +// swagger:operation GET /status getStatus +// +// Get the current status of the RAUC updater +// +// --- +// produces: [application/json] +// responses: +// 200: +// description: Ok +// schema: +// $ref: "#/definitions/RaucStatus" +// 500: +// description: Server Error +// schema: +// $ref: "#/definitions/Error" func (srv *SEBRAUCServer) controllerStatus(c *gin.Context) { - // swagger:operation GET /status getStatus - // - // Get the current status of the RAUC updater - // - // --- - // produces: [application/json] - // responses: - // 200: - // description: Ok - // schema: - // $ref: "#/definitions/RaucStatus" - // 500: - // description: Server Error - // schema: - // $ref: "#/definitions/StatusMessage" - - c.JSON(200, srv.raucUpdater.GetStatus()) + c.JSON(http.StatusOK, srv.udater.GetStatus()) } +// swagger:operation GET /info getInfo +// +// Get the current system info +// +// --- +// produces: [application/json] +// responses: +// 200: +// description: Ok +// schema: +// $ref: "#/definitions/SystemInfo" +// 500: +// description: Server Error +// schema: +// $ref: "#/definitions/Error" func (srv *SEBRAUCServer) controllerInfo(c *gin.Context) { - // swagger:operation GET /info getInfo - // - // Get the current system info - // - // --- - // produces: [application/json] - // responses: - // 200: - // description: Ok - // schema: - // $ref: "#/definitions/SystemInfo" - // 500: - // description: Server Error - // schema: - // $ref: "#/definitions/StatusMessage" - info, err := sysinfo.GetSysinfo() if err != nil { c.Error(err) } else { - c.JSON(200, info) + c.JSON(http.StatusOK, info) } } +// swagger:operation GET /reboot startReboot +// +// Reboot the system +// +// --- +// produces: [application/json] +// responses: +// 200: +// description: Ok +// schema: +// $ref: "#/definitions/StatusMessage" +// 500: +// description: Server Error +// schema: +// $ref: "#/definitions/Error" func (srv *SEBRAUCServer) controllerReboot(c *gin.Context) { - // swagger:operation GET /reboot startReboot - // - // Reboot the system - // - // --- - // produces: [application/json] - // responses: - // 200: - // description: Ok - // schema: - // $ref: "#/definitions/StatusMessage" - // 500: - // description: Server Error - // schema: - // $ref: "#/definitions/StatusMessage" - go util.Reboot(5 * time.Second) writeStatus(c, true, "System is rebooting") } -func errorHandler(c *gin.Context, err error) error { - // API error handling - if strings.HasPrefix(c.FullPath(), "/api") { - writeStatus(c, false, err.Error()) - } - return err +// controllerError throws an error for testing +func (srv *SEBRAUCServer) controllerError(c *gin.Context) { + c.Error(util.HttpErrNew("error test", http.StatusBadRequest)) +} + +// controllerPanic panics for testing +func (srv *SEBRAUCServer) controllerPanic(c *gin.Context) { + panic(errors.New("panic message")) } func writeStatus(c *gin.Context, success bool, msg string) { - status := 200 - - if !success { - status = 500 - } - - c.JSON(status, model.StatusMessage{ + c.JSON(http.StatusOK, model.StatusMessage{ Success: success, Msg: msg, }) diff --git a/src/server/swagger/docs.go b/src/server/swagger/docs.go deleted file mode 100644 index 198db73..0000000 --- a/src/server/swagger/docs.go +++ /dev/null @@ -1,11 +0,0 @@ -// SEBRAUC -// -// REST API for the SEBRAUC firmware updater -// -// --- -// Schemes: http, https -// Version: 0.2.0 -// License: MIT -// -// swagger:meta -package swagger diff --git a/src/server/swagger/swagger.go b/src/server/swagger/swagger.go index 732f009..e48ca00 100644 --- a/src/server/swagger/swagger.go +++ b/src/server/swagger/swagger.go @@ -3,6 +3,7 @@ package swagger import ( _ "embed" + "code.thetadev.de/TSGRain/SEBRAUC/src/server/middleware" "github.com/gin-gonic/gin" ) @@ -13,7 +14,7 @@ var swaggerHtml []byte var swaggerYaml []byte func Register(r *gin.Engine) { - swg := r.Group("/api/swagger") + swg := r.Group("/api/swagger", middleware.Cache) swg.GET("/", func(c *gin.Context) { c.Data(200, "text/html", swaggerHtml) diff --git a/src/server/swagger/swagger.yaml b/src/server/swagger/swagger.yaml index 24989f5..3d85982 100644 --- a/src/server/swagger/swagger.yaml +++ b/src/server/swagger/swagger.yaml @@ -1,5 +1,28 @@ definitions: + Error: + description: The Error contains error relevant information. + properties: + error: + description: The general error message according to HTTP specification. + example: Unauthorized + type: string + msg: + description: Concrete error message. + example: already running + type: string + status_code: + description: The http error code. + example: 500 + format: int64 + type: integer + required: + - error + - status_code + - msg + title: Error model + type: object RaucStatus: + description: RaucStatus contains information about the current RAUC updater status. properties: installing: description: True if the installer is running @@ -29,6 +52,7 @@ definitions: - message - last_error - log + title: RaucStatus model type: object Rootfs: properties: @@ -62,6 +86,7 @@ definitions: - primary type: object StatusMessage: + description: StatusMessage contains the status of an operation. properties: msg: description: Status message text @@ -73,8 +98,10 @@ definitions: required: - success - msg + title: StatusMessage model type: object SystemInfo: + description: SystemInfo contains information about the running system. properties: hostname: description: Hostname of the system @@ -114,6 +141,7 @@ definitions: - rauc_compatible - rauc_variant - rauc_rootfs + title: SystemInfo model type: object info: description: REST API for the SEBRAUC firmware updater @@ -136,7 +164,7 @@ paths: "500": description: Server Error schema: - $ref: "#/definitions/StatusMessage" + $ref: "#/definitions/Error" /reboot: get: description: Reboot the system @@ -151,7 +179,7 @@ paths: "500": description: Server Error schema: - $ref: "#/definitions/StatusMessage" + $ref: "#/definitions/Error" /status: get: description: Get the current status of the RAUC updater @@ -166,7 +194,7 @@ paths: "500": description: Server Error schema: - $ref: "#/definitions/StatusMessage" + $ref: "#/definitions/Error" /update: post: consumes: @@ -174,7 +202,7 @@ paths: description: Start the update process operationId: startUpdate parameters: - - description: Rauc firmware image file (*.raucb) + - description: RAUC firmware image file (*.raucb) in: formData name: updateFile required: true @@ -186,10 +214,12 @@ paths: description: Ok schema: $ref: "#/definitions/StatusMessage" + "409": + description: already running "500": description: Server Error schema: - $ref: "#/definitions/StatusMessage" + $ref: "#/definitions/Error" schemes: - http - https diff --git a/src/util/commands.go b/src/util/commands.go index a157fa0..78fb45d 100644 --- a/src/util/commands.go +++ b/src/util/commands.go @@ -3,9 +3,11 @@ package util +import "code.thetadev.de/TSGRain/SEBRAUC/src/server/mode" + const ( RebootCmd = "shutdown -r 0" RaucCmd = "rauc" - TestMode = false + Mode = mode.Prod ) diff --git a/src/util/commands_mock.go b/src/util/commands_mock.go index d4d1392..b447055 100644 --- a/src/util/commands_mock.go +++ b/src/util/commands_mock.go @@ -3,9 +3,11 @@ package util +import "code.thetadev.de/TSGRain/SEBRAUC/src/server/mode" + const ( RebootCmd = "touch /tmp/sebrauc_reboot_test" RaucCmd = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock" - TestMode = true + Mode = mode.Dev ) diff --git a/src/util/errors.go b/src/util/errors.go index 20ee9e4..1d3fc3b 100644 --- a/src/util/errors.go +++ b/src/util/errors.go @@ -1,8 +1,12 @@ package util -import "errors" +import ( + "errors" + "net/http" +) var ( ErrAlreadyRunning = errors.New("rauc already running") ErrFileDoesNotExist = errors.New("file does not exist") + ErrPageNotFound = HttpErrNew("page not found", http.StatusNotFound) ) diff --git a/src/util/http_error.go b/src/util/http_error.go new file mode 100644 index 0000000..c5d7cf8 --- /dev/null +++ b/src/util/http_error.go @@ -0,0 +1,39 @@ +package util + +import "errors" + +type HttpError interface { + error + StatusCode() int +} + +type httpErr struct { + err error + statusCode int +} + +func HttpErrWrap(e error, statusCode int) HttpError { + return &httpErr{ + err: e, + statusCode: statusCode, + } +} + +func HttpErrNew(msg string, statusCode int) HttpError { + return HttpErrWrap(errors.New(msg), statusCode) +} + +func (e *httpErr) Error() string { + if e.err == nil { + return "" + } + return e.err.Error() +} + +func (e *httpErr) Unwrap() error { + return e.err +} + +func (e *httpErr) StatusCode() int { + return e.statusCode +} diff --git a/src/util/types.go b/src/util/types.go new file mode 100644 index 0000000..afd106b --- /dev/null +++ b/src/util/types.go @@ -0,0 +1,5 @@ +package util + +type Broadcaster interface { + Broadcast(msg []byte) +} diff --git a/ui/src/sebrauc-client/api.ts b/ui/src/sebrauc-client/api.ts index 015ad91..c98ab0a 100644 --- a/ui/src/sebrauc-client/api.ts +++ b/ui/src/sebrauc-client/api.ts @@ -38,7 +38,32 @@ import { } from "./base" /** - * + * The Error contains error relevant information. + * @export + * @interface ModelError + */ +export interface ModelError { + /** + * The general error message according to HTTP specification. + * @type {string} + * @memberof ModelError + */ + error: string + /** + * Concrete error message. + * @type {string} + * @memberof ModelError + */ + msg: string + /** + * The http error code. + * @type {number} + * @memberof ModelError + */ + status_code: number +} +/** + * RaucStatus contains information about the current RAUC updater status. * @export * @interface RaucStatus */ @@ -118,7 +143,7 @@ export interface Rootfs { type: string } /** - * + * StatusMessage contains the status of an operation. * @export * @interface StatusMessage */ @@ -137,7 +162,7 @@ export interface StatusMessage { success: boolean } /** - * + * SystemInfo contains information about the running system. * @export * @interface SystemInfo */ @@ -290,7 +315,7 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati }, /** * Start the update process - * @param {any} updateFile Rauc firmware image file (*.raucb) + * @param {any} updateFile RAUC firmware image file (*.raucb) * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -404,7 +429,7 @@ export const DefaultApiFp = function (configuration?: Configuration) { }, /** * Start the update process - * @param {any} updateFile Rauc firmware image file (*.raucb) + * @param {any} updateFile RAUC firmware image file (*.raucb) * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -471,7 +496,7 @@ export const DefaultApiFactory = function ( }, /** * Start the update process - * @param {any} updateFile Rauc firmware image file (*.raucb) + * @param {any} updateFile RAUC firmware image file (*.raucb) * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -528,7 +553,7 @@ export class DefaultApi extends BaseAPI { /** * Start the update process - * @param {any} updateFile Rauc firmware image file (*.raucb) + * @param {any} updateFile RAUC firmware image file (*.raucb) * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi diff --git a/ui/ui.go b/ui/ui.go index a10fda6..66329e8 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -7,6 +7,7 @@ import ( "io/fs" "net/http" + "code.thetadev.de/TSGRain/SEBRAUC/src/server/middleware" "code.thetadev.de/TSGRain/SEBRAUC/src/util" "code.thetadev.de/TSGRain/ginzip" "github.com/gin-gonic/gin" @@ -36,7 +37,7 @@ func distFS() fs.FS { func Register(r *gin.Engine) { indexHandler := getIndexHandler() - ui := r.Group("/", ginzip.New(ginzip.DefaultOptions())) + ui := r.Group("/", ginzip.New(ginzip.DefaultOptions()), middleware.Cache) ui.GET("/", indexHandler) ui.GET("/index.html", indexHandler)