Compare commits

...

2 commits

Author SHA1 Message Date
71764dd6fa add caching/error handling middleware
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-19 17:05:58 +01:00
e1c4c58684 add swagger page 2021-12-18 00:39:08 +01:00
26 changed files with 442 additions and 283 deletions

View file

@ -9,14 +9,6 @@ repos:
- repo: local - repo: local
hooks: hooks:
- id: check-swagger
name: check-swagger
language: system
files: swagger.yaml
entry: swagger
args: ["validate", "swagger.yaml"]
pass_filenames: false
- id: tsc - id: tsc
name: tsc name: tsc
entry: tsc entry: tsc

View file

@ -1,6 +1,8 @@
SRC_DIR=./src SRC_DIR=./src
UI_DIR=./ui UI_DIR=./ui
APIDOC_FILE=./src/server/swagger/swagger.yaml
VERSION=$(shell git tag --sort=-version:refname | head -n 1) VERSION=$(shell git tag --sort=-version:refname | head -n 1)
setup: setup:
@ -22,10 +24,10 @@ build-server:
build: build-ui build-server build: build-ui build-server
generate-apidoc: 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: 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 cd ${UI_DIR} && npm run format
clean: clean:

2
go.mod
View file

@ -4,9 +4,11 @@ go 1.16
require ( require (
code.thetadev.de/TSGRain/ginzip v0.1.1 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/fortytw2/leaktest v1.3.0
github.com/gin-contrib/cors v1.3.1 github.com/gin-contrib/cors v1.3.1
github.com/gin-gonic/gin v1.7.7 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/google/uuid v1.3.0
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0

4
go.sum
View file

@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= 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.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 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= 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 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=

View file

@ -1,8 +0,0 @@
package assets
import (
"embed"
)
//go:embed files/**
var Assets embed.FS

View file

@ -1,88 +0,0 @@
<!-- See https://github.com/gorilla/websocket/blob/master/examples/chat/home.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Chat Example</title>
<script type="text/javascript">
window.onload = function () {
var conn
var msg = document.getElementById("msg")
var log = document.getElementById("log")
function appendLog(item) {
var doScroll =
log.scrollTop > log.scrollHeight - log.clientHeight - 1
log.appendChild(item)
if (doScroll) {
log.scrollTop = log.scrollHeight - log.clientHeight
}
}
if (window["WebSocket"]) {
conn = new WebSocket("ws://" + document.location.host + "/api/ws")
conn.onclose = function (evt) {
var item = document.createElement("div")
item.innerHTML = "<b>Connection closed.</b>"
appendLog(item)
}
conn.onmessage = function (evt) {
var messages = evt.data.split("\n")
for (var i = 0; i < messages.length; i++) {
var item = document.createElement("div")
item.innerText = messages[i]
appendLog(item)
}
}
} else {
var item = document.createElement("div")
item.innerHTML = "<b>Your browser does not support WebSockets.</b>"
appendLog(item)
}
}
</script>
<style type="text/css">
html {
overflow: hidden;
}
body {
overflow: hidden;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
background: gray;
}
#log {
background: white;
margin: 0;
padding: 0.5em 0.5em 0.5em 0.5em;
position: absolute;
top: 4em;
left: 0.5em;
right: 0.5em;
bottom: 3em;
overflow: auto;
}
#form {
padding: 0 0.5em 0 0.5em;
margin: 0;
position: absolute;
bottom: 1em;
left: 0px;
width: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div id="log"></div>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="updateFile" id="file" />
<input type="submit" name="submit" />
</form>
</body>
</html>

View file

@ -3,20 +3,28 @@ package main
import ( import (
"fmt" "fmt"
"log" "log"
"time"
"code.thetadev.de/TSGRain/SEBRAUC/src/rauc"
"code.thetadev.de/TSGRain/SEBRAUC/src/server" "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" "code.thetadev.de/TSGRain/SEBRAUC/src/util"
) )
func main() { func main() {
fmt.Println("SEBRAUC " + util.Version()) 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("Test mode active - no update operations are executed.")
fmt.Println("Build with -tags prod to enable live mode.") 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() err := srv.Run()
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)

24
src/model/error.go Normal file
View file

@ -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"`
}

View file

@ -1,5 +1,9 @@
package model package model
// RaucStatus model
//
// RaucStatus contains information about the current RAUC updater status.
//
//swagger:model RaucStatus //swagger:model RaucStatus
type RaucStatus struct { type RaucStatus struct {
// True if the installer is running // True if the installer is running
@ -19,13 +23,11 @@ type RaucStatus struct {
// Installation error message // Installation error message
// required: true // required: true
//nolint:lll // example: Failed to check bundle identifier: Invalid identifier.
// example: Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle?
LastError string `json:"last_error"` LastError string `json:"last_error"`
// Full command line output of the current installation // Full command line output of the current installation
// required: true // required: true
//nolint:lll // example: 0% Installing 0% Determining slot states 20% Determining slot states done
// example: 0% Installing\n0% Determining slot states\n20% Determining slot states done\n
Log string `json:"log"` Log string `json:"log"`
} }

View file

@ -1,5 +1,9 @@
package model package model
// StatusMessage model
//
// StatusMessage contains the status of an operation.
//
//swagger:model StatusMessage //swagger:model StatusMessage
type StatusMessage struct { type StatusMessage struct {
// Is operation successful? // Is operation successful?

View file

@ -1,5 +1,9 @@
package model package model
// SystemInfo model
//
// SystemInfo contains information about the running system.
//
//swagger:model SystemInfo //swagger:model SystemInfo
type SystemInfo struct { type SystemInfo struct {
// Hostname of the system // Hostname of the system

View file

@ -19,23 +19,14 @@ var (
) )
type Rauc struct { type Rauc struct {
bc broadcaster bc util.Broadcaster
status model.RaucStatus status model.RaucStatus
runningMtx sync.Mutex runningMtx sync.Mutex
} }
type broadcaster interface { func (r *Rauc) SetBroadcaster(bc util.Broadcaster) {
Broadcast(msg []byte) r.bc = bc
}
func NewRauc(bc broadcaster) *Rauc {
r := &Rauc{
bc: bc,
}
r.bc.Broadcast(r.GetStatusJson()) r.bc.Broadcast(r.GetStatusJson())
return r
} }
func (r *Rauc) completed(updateFile string) { func (r *Rauc) completed(updateFile string) {

View file

@ -1,17 +0,0 @@
// SEBRAUC
//
// REST API for the SEBRAUC firmware updater
//
// ---
// Schemes: http, https
// Version: 0.2.0
// License: MIT
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// swagger:meta
package server

View file

@ -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")
}

View file

@ -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())
}
}

View file

@ -1,15 +1,25 @@
// SEBRAUC
//
// REST API for the SEBRAUC firmware updater
//
// ---
// Schemes: http, https
// Version: 0.2.0
// License: MIT
//
// swagger:meta
package server package server
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings" "net/http"
"time" "time"
"code.thetadev.de/TSGRain/SEBRAUC/src/model" "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/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/sysinfo"
"code.thetadev.de/TSGRain/SEBRAUC/src/util" "code.thetadev.de/TSGRain/SEBRAUC/src/util"
"code.thetadev.de/TSGRain/SEBRAUC/ui" "code.thetadev.de/TSGRain/SEBRAUC/ui"
@ -18,17 +28,28 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
type SEBRAUCServer struct { type serverStreamer interface {
address string util.Broadcaster
raucUpdater *rauc.Rauc Handle(ctx *gin.Context)
streamer *stream.API Close()
tmpdir string
} }
func NewServer(address string) *SEBRAUCServer { type serverUpdater interface {
streamer := stream.New(10*time.Second, 1*time.Second, []string{}) 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() tmpdir, err := util.GetTmpdir()
if err != nil { if err != nil {
@ -36,68 +57,80 @@ func NewServer(address string) *SEBRAUCServer {
} }
return &SEBRAUCServer{ return &SEBRAUCServer{
address: address, address: address,
raucUpdater: raucUpdater, udater: updater,
streamer: streamer, streamer: streamer,
tmpdir: tmpdir, tmpdir: tmpdir,
} }
} }
func (srv *SEBRAUCServer) Run() error { func (srv *SEBRAUCServer) Run() error {
router := gin.Default() router := gin.New()
router.Use(gin.Logger())
_ = router.SetTrustedProxies(nil)
if mode.IsDev() { if mode.IsDev() {
router.Use(cors.Default()) router.Use(cors.Default())
} }
router.Use(middleware.ErrorHandler(false), middleware.PanicHandler(false))
router.NoRoute(func(c *gin.Context) { c.Error(util.ErrPageNotFound) })
api := router.Group("/api",
middleware.ErrorHandler(true), middleware.PanicHandler(true))
// ROUTES // ROUTES
router.GET("/api/ws", srv.streamer.Handle) api.GET("/ws", srv.streamer.Handle)
router.GET("/api/status", srv.controllerStatus) api.GET("/status", srv.controllerStatus)
router.GET("/api/info", srv.controllerInfo) api.GET("/info", srv.controllerInfo)
router.POST("/api/update", srv.controllerUpdate) api.POST("/update", srv.controllerUpdate)
router.POST("/api/reboot", srv.controllerReboot) 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)
}
// router.StaticFS("/", ui.GetFS())
ui.Register(router) ui.Register(router)
swagger.Register(router)
return router.Run(srv.address) return router.Run(srv.address)
} }
// @Description Start the update process // swagger:operation POST /update startUpdate
// @Route /update
// //
// _Param {name} {in} {goType} {required} {description} // Start the update process
// @Param updateFile form file true "Rauc firmware image file (*.raucb)"
// //
// _Success {status} {jsonType} {goType} {description} // ---
// @Success 200 object statusMessage // consumes:
// @Failure 500 object statusMessage "Server error" // - 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) { 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") file, err := c.FormFile("updateFile")
if err != nil { if err != nil {
c.Error(err) c.Error(err)
@ -118,100 +151,93 @@ func (srv *SEBRAUCServer) controllerUpdate(c *gin.Context) {
return return
} }
err = srv.raucUpdater.RunRauc(updateFile) err = srv.udater.RunRauc(updateFile)
if err == nil { if err == nil {
writeStatus(c, true, "Update started") writeStatus(c, true, "Update started")
} else if errors.Is(err, util.ErrAlreadyRunning) { } else if errors.Is(err, util.ErrAlreadyRunning) {
c.AbortWithError(409, errors.New("already running")) writeStatus(c, false, "Updater already running")
} else { } else {
c.Error(err) c.Error(err)
return 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) { func (srv *SEBRAUCServer) controllerStatus(c *gin.Context) {
// swagger:operation GET /status getStatus c.JSON(http.StatusOK, srv.udater.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())
} }
// 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) { 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() info, err := sysinfo.GetSysinfo()
if err != nil { if err != nil {
c.Error(err) c.Error(err)
} else { } 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) { 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) go util.Reboot(5 * time.Second)
writeStatus(c, true, "System is rebooting") writeStatus(c, true, "System is rebooting")
} }
func errorHandler(c *gin.Context, err error) error { // controllerError throws an error for testing
// API error handling func (srv *SEBRAUCServer) controllerError(c *gin.Context) {
if strings.HasPrefix(c.FullPath(), "/api") { c.Error(util.HttpErrNew("error test", http.StatusBadRequest))
writeStatus(c, false, err.Error()) }
}
return err // 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) { func writeStatus(c *gin.Context, success bool, msg string) {
status := 200 c.JSON(http.StatusOK, model.StatusMessage{
if !success {
status = 500
}
c.JSON(status, model.StatusMessage{
Success: success, Success: success,
Msg: msg, Msg: msg,
}) })

View file

@ -0,0 +1,25 @@
package swagger
import (
_ "embed"
"code.thetadev.de/TSGRain/SEBRAUC/src/server/middleware"
"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", middleware.Cache)
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)
})
}

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>SEBRAUC API documentation</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url="swagger.yaml"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"></script>
</body>
</html>

View file

@ -1,20 +1,40 @@
consumes:
- application/json
definitions: 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: RaucStatus:
description: RaucStatus contains information about the current RAUC updater status.
properties: properties:
installing: installing:
description: True if the installer is running description: True if the installer is running
type: boolean type: boolean
last_error: last_error:
description: Installation error message description: Installation error message
example: "Failed to check bundle identifier: Invalid identifier. Did you pass example: "Failed to check bundle identifier: Invalid identifier."
a valid RAUC bundle?"
type: string type: string
log: log:
description: Full command line output of the current installation description: Full command line output of the current installation
example: 0% Installing\n0% Determining slot states\n20% Determining slot states example: 0% Installing 0% Determining slot states 20% Determining slot states
done\n done
type: string type: string
message: message:
description: Current installation step description: Current installation step
@ -32,6 +52,7 @@ definitions:
- message - message
- last_error - last_error
- log - log
title: RaucStatus model
type: object type: object
Rootfs: Rootfs:
properties: properties:
@ -65,6 +86,7 @@ definitions:
- primary - primary
type: object type: object
StatusMessage: StatusMessage:
description: StatusMessage contains the status of an operation.
properties: properties:
msg: msg:
description: Status message text description: Status message text
@ -76,8 +98,10 @@ definitions:
required: required:
- success - success
- msg - msg
title: StatusMessage model
type: object type: object
SystemInfo: SystemInfo:
description: SystemInfo contains information about the running system.
properties: properties:
hostname: hostname:
description: Hostname of the system description: Hostname of the system
@ -117,6 +141,7 @@ definitions:
- rauc_compatible - rauc_compatible
- rauc_variant - rauc_variant
- rauc_rootfs - rauc_rootfs
title: SystemInfo model
type: object type: object
info: info:
description: REST API for the SEBRAUC firmware updater description: REST API for the SEBRAUC firmware updater
@ -139,7 +164,7 @@ paths:
"500": "500":
description: Server Error description: Server Error
schema: schema:
$ref: "#/definitions/StatusMessage" $ref: "#/definitions/Error"
/reboot: /reboot:
get: get:
description: Reboot the system description: Reboot the system
@ -154,7 +179,7 @@ paths:
"500": "500":
description: Server Error description: Server Error
schema: schema:
$ref: "#/definitions/StatusMessage" $ref: "#/definitions/Error"
/status: /status:
get: get:
description: Get the current status of the RAUC updater description: Get the current status of the RAUC updater
@ -169,7 +194,7 @@ paths:
"500": "500":
description: Server Error description: Server Error
schema: schema:
$ref: "#/definitions/StatusMessage" $ref: "#/definitions/Error"
/update: /update:
post: post:
consumes: consumes:
@ -177,7 +202,7 @@ paths:
description: Start the update process description: Start the update process
operationId: startUpdate operationId: startUpdate
parameters: parameters:
- description: Rauc firmware image file (*.raucb) - description: RAUC firmware image file (*.raucb)
in: formData in: formData
name: updateFile name: updateFile
required: true required: true
@ -189,12 +214,12 @@ paths:
description: Ok description: Ok
schema: schema:
$ref: "#/definitions/StatusMessage" $ref: "#/definitions/StatusMessage"
"409":
description: already running
"500": "500":
description: Server Error description: Server Error
schema: schema:
$ref: "#/definitions/StatusMessage" $ref: "#/definitions/Error"
produces:
- application/json
schemes: schemes:
- http - http
- https - https

View file

@ -3,9 +3,11 @@
package util package util
import "code.thetadev.de/TSGRain/SEBRAUC/src/server/mode"
const ( const (
RebootCmd = "shutdown -r 0" RebootCmd = "shutdown -r 0"
RaucCmd = "rauc" RaucCmd = "rauc"
TestMode = false Mode = mode.Prod
) )

View file

@ -3,9 +3,11 @@
package util package util
import "code.thetadev.de/TSGRain/SEBRAUC/src/server/mode"
const ( const (
RebootCmd = "touch /tmp/sebrauc_reboot_test" RebootCmd = "touch /tmp/sebrauc_reboot_test"
RaucCmd = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock" RaucCmd = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock"
TestMode = true Mode = mode.Dev
) )

View file

@ -1,8 +1,12 @@
package util package util
import "errors" import (
"errors"
"net/http"
)
var ( var (
ErrAlreadyRunning = errors.New("rauc already running") ErrAlreadyRunning = errors.New("rauc already running")
ErrFileDoesNotExist = errors.New("file does not exist") ErrFileDoesNotExist = errors.New("file does not exist")
ErrPageNotFound = HttpErrNew("page not found", http.StatusNotFound)
) )

39
src/util/http_error.go Normal file
View file

@ -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
}

5
src/util/types.go Normal file
View file

@ -0,0 +1,5 @@
package util
type Broadcaster interface {
Broadcast(msg []byte)
}

View file

@ -38,7 +38,32 @@ import {
} from "./base" } 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 * @export
* @interface RaucStatus * @interface RaucStatus
*/ */
@ -118,7 +143,7 @@ export interface Rootfs {
type: string type: string
} }
/** /**
* * StatusMessage contains the status of an operation.
* @export * @export
* @interface StatusMessage * @interface StatusMessage
*/ */
@ -137,7 +162,7 @@ export interface StatusMessage {
success: boolean success: boolean
} }
/** /**
* * SystemInfo contains information about the running system.
* @export * @export
* @interface SystemInfo * @interface SystemInfo
*/ */
@ -290,7 +315,7 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
}, },
/** /**
* Start the update process * 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
@ -404,7 +429,7 @@ export const DefaultApiFp = function (configuration?: Configuration) {
}, },
/** /**
* Start the update process * 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
@ -471,7 +496,7 @@ export const DefaultApiFactory = function (
}, },
/** /**
* Start the update process * 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
@ -528,7 +553,7 @@ export class DefaultApi extends BaseAPI {
/** /**
* Start the update process * 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
* @memberof DefaultApi * @memberof DefaultApi

View file

@ -7,6 +7,7 @@ import (
"io/fs" "io/fs"
"net/http" "net/http"
"code.thetadev.de/TSGRain/SEBRAUC/src/server/middleware"
"code.thetadev.de/TSGRain/SEBRAUC/src/util" "code.thetadev.de/TSGRain/SEBRAUC/src/util"
"code.thetadev.de/TSGRain/ginzip" "code.thetadev.de/TSGRain/ginzip"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -36,7 +37,7 @@ func distFS() fs.FS {
func Register(r *gin.Engine) { func Register(r *gin.Engine) {
indexHandler := getIndexHandler() indexHandler := getIndexHandler()
ui := r.Group("/", ginzip.New(ginzip.DefaultOptions())) ui := r.Group("/", ginzip.New(ginzip.DefaultOptions()), middleware.Cache)
ui.GET("/", indexHandler) ui.GET("/", indexHandler)
ui.GET("/index.html", indexHandler) ui.GET("/index.html", indexHandler)