integrated config

fixed method type for /reboot in apidoc
This commit is contained in:
Theta-Dev 2021-12-24 01:55:16 +01:00
parent 312de77236
commit acd1db7363
23 changed files with 183 additions and 121 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
build
tmp
sebrauc.toml

View file

@ -3,6 +3,7 @@ package config
import (
"strings"
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd"
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
"code.thetadev.de/TSGRain/SEBRAUC/src/util/mode"
"github.com/jinzhu/configor"
@ -86,11 +87,12 @@ func stripTrailingSlashes(pathIn string) string {
// env variables or config file.
func GetWithFlags(pathIn string, portIn int) *Config {
cfg := new(Config)
err := configor.New(&configor.Config{
// Debug: true,
// Verbose: true,
cfgor := configor.New(&configor.Config{
// Debug: true,
ENVPrefix: "SEBRAUC",
}).Load(cfg, findConfigFile(pathIn))
})
err := cfgor.Load(cfg, findConfigFile(pathIn))
if err != nil {
panic(err)
}
@ -104,11 +106,9 @@ func GetWithFlags(pathIn string, portIn int) *Config {
// Override commands with testing options
if mode.IsDev() {
//nolint:lll
cfg.Commands.RaucStatus = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock status --output-format=json"
//nolint:lll
cfg.Commands.RaucInstall = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock install"
cfg.Commands.Reboot = "touch /tmp/sebrauc_reboot_test"
cfg.Commands.RaucStatus = testcmd.RaucStatus
cfg.Commands.RaucInstall = testcmd.RaucInstall
cfg.Commands.Reboot = testcmd.Reboot
}
return cfg

View file

@ -0,0 +1,8 @@
package testcmd
//nolint:lll
const (
RaucStatus = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock status --output-format=json"
RaucInstall = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock install"
Reboot = "touch /tmp/sebrauc_reboot_test"
)

View file

@ -3,27 +3,32 @@ package main
import (
"fmt"
"log"
"time"
"code.thetadev.de/TSGRain/SEBRAUC/src/rauc"
"code.thetadev.de/TSGRain/SEBRAUC/src/config"
"code.thetadev.de/TSGRain/SEBRAUC/src/server"
"code.thetadev.de/TSGRain/SEBRAUC/src/server/stream"
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
"code.thetadev.de/TSGRain/SEBRAUC/src/util/mode"
)
const titleArt = ` _____ __________ ____ ___ __ ________
/ ___// ____/ __ )/ __ \/ | / / / / ____/
\__ \/ __/ / __ / /_/ / /| |/ / / / /
___/ / /___/ /_/ / _, _/ ___ / /_/ / /___
/____/_____/_____/_/ |_/_/ |_\____/\____/ `
func main() {
fmt.Println("SEBRAUC " + util.Version())
fmt.Println(titleArt + util.Version() + "\n")
if mode.IsDev() {
fmt.Println("Test mode active - no update operations are executed.")
fmt.Println("Build with -tags prod to enable live mode.")
}
streamer := stream.New(10*time.Second, 1*time.Second, []string{})
raucUpdater := &rauc.Rauc{}
cfg := config.GetWithFlags("", 8080)
srv := server.NewServer(":8080", streamer, raucUpdater)
fmt.Printf("Starting server at %s:%d\n", cfg.Server.Address, cfg.Server.Port)
srv := server.NewServer(cfg)
err := srv.Run()
if err != nil {
log.Fatalln(err)

View file

@ -19,9 +19,16 @@ var (
)
type Rauc struct {
bc util.Broadcaster
status model.RaucStatus
runningMtx sync.Mutex
cmdRaucInstall string
bc util.Broadcaster
status model.RaucStatus
runningMtx sync.Mutex
}
func New(cmdRaucInstall string) *Rauc {
return &Rauc{
cmdRaucInstall: cmdRaucInstall,
}
}
func (r *Rauc) SetBroadcaster(bc util.Broadcaster) {
@ -58,8 +65,7 @@ func (r *Rauc) RunRauc(updateFile string) error {
}
r.bcStatus()
cmd := util.CommandFromString(
fmt.Sprintf("%s install %s", util.RaucCmd, updateFile))
cmd := util.CommandFromString(r.cmdRaucInstall + " " + updateFile)
readPipe, _ := cmd.StdoutPipe()
cmd.Stderr = cmd.Stdout

View file

@ -6,6 +6,7 @@ import (
"testing"
"time"
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd"
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
"github.com/stretchr/testify/assert"
)
@ -75,7 +76,7 @@ func TestRauc(t *testing.T) {
os.Setenv("RAUC_MOCK_TEST", "1")
os.Setenv("RAUC_MOCK_FAIL", tt.fail)
updater := &Rauc{}
updater := New(testcmd.RaucInstall)
bc := &broadcasterMock{}
updater.SetBroadcaster(bc)
@ -101,7 +102,7 @@ func TestRauc(t *testing.T) {
}
func createTmpfile() string {
tmpdir, err := util.GetTmpdir()
tmpdir, err := util.GetTmpdir("/tmp")
if err != nil {
panic(err)
}

View file

@ -0,0 +1,14 @@
package middleware
import (
"code.thetadev.de/TSGRain/ginzip"
"github.com/gin-gonic/gin"
)
func Compression(gzip, brotli string) gin.HandlerFunc {
opts := ginzip.DefaultOptions()
opts.GzipLevel = gzip
opts.BrotliLevel = brotli
return ginzip.New(opts)
}

View file

@ -16,8 +16,11 @@ import (
"net/http"
"time"
"code.thetadev.de/TSGRain/SEBRAUC/src/config"
"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/stream"
"code.thetadev.de/TSGRain/SEBRAUC/src/server/swagger"
"code.thetadev.de/TSGRain/SEBRAUC/src/sysinfo"
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
@ -28,38 +31,42 @@ import (
"github.com/google/uuid"
)
type serverStreamer interface {
util.Broadcaster
Handle(ctx *gin.Context)
Close()
}
type serverUpdater interface {
GetStatus() model.RaucStatus
RunRauc(updateFile string) error
SetBroadcaster(bc util.Broadcaster)
}
type SEBRAUCServer struct {
address string
streamer serverStreamer
udater serverUpdater
config *config.Config
streamer *stream.API
updater *rauc.Rauc
sysinfo *sysinfo.Sysinfo
tmpdir string
}
func NewServer(address string, streamer serverStreamer,
updater serverUpdater) *SEBRAUCServer {
func NewServer(config *config.Config) *SEBRAUCServer {
updater := rauc.New(config.Commands.RaucInstall)
streamer := stream.New(
time.Duration(config.Server.Websocket.Ping)*time.Second,
time.Duration(config.Server.Websocket.Timeout)*time.Second,
[]string{},
)
sysinfo := sysinfo.New(
config.Commands.RaucStatus,
config.Sysinfo.ReleaseFile,
config.Sysinfo.NameKey,
config.Sysinfo.VersionKey,
config.Sysinfo.HostnameFile,
config.Sysinfo.UptimeFile,
)
updater.SetBroadcaster(streamer)
tmpdir, err := util.GetTmpdir()
tmpdir, err := util.GetTmpdir(config.Tmpdir)
if err != nil {
panic(err)
}
return &SEBRAUCServer{
address: address,
udater: updater,
config: config,
updater: updater,
streamer: streamer,
sysinfo: sysinfo,
tmpdir: tmpdir,
}
}
@ -79,7 +86,7 @@ func (srv *SEBRAUCServer) Run() error {
api := router.Group("/api",
middleware.ErrorHandler(true), middleware.PanicHandler(true))
// ROUTES
// API ROUTES
api.GET("/ws", srv.streamer.Handle)
api.GET("/status", srv.controllerStatus)
api.GET("/info", srv.controllerInfo)
@ -96,11 +103,16 @@ func (srv *SEBRAUCServer) Run() error {
api.GET("/panic", srv.controllerPanic)
}
ui.Register(router)
// UI
uiGroup := router.Group("/", middleware.Compression(
srv.config.Server.Compression.Gzip,
srv.config.Server.Compression.Brotli),
)
ui.Register(uiGroup)
swagger.Register(uiGroup)
swagger.Register(router)
return router.Run(srv.address)
return router.Run(fmt.Sprintf("%s:%d",
srv.config.Server.Address, srv.config.Server.Port))
}
// swagger:operation POST /update startUpdate
@ -151,7 +163,7 @@ func (srv *SEBRAUCServer) controllerUpdate(c *gin.Context) {
return
}
err = srv.udater.RunRauc(updateFile)
err = srv.updater.RunRauc(updateFile)
if err == nil {
writeStatus(c, true, "Update started")
} else {
@ -176,7 +188,7 @@ func (srv *SEBRAUCServer) controllerUpdate(c *gin.Context) {
// schema:
// $ref: "#/definitions/Error"
func (srv *SEBRAUCServer) controllerStatus(c *gin.Context) {
c.JSON(http.StatusOK, srv.udater.GetStatus())
c.JSON(http.StatusOK, srv.updater.GetStatus())
}
// swagger:operation GET /info getInfo
@ -195,7 +207,7 @@ func (srv *SEBRAUCServer) controllerStatus(c *gin.Context) {
// schema:
// $ref: "#/definitions/Error"
func (srv *SEBRAUCServer) controllerInfo(c *gin.Context) {
info, err := sysinfo.GetSysinfo()
info, err := srv.sysinfo.GetSysinfo()
if err != nil {
c.Error(err)
} else {
@ -203,7 +215,7 @@ func (srv *SEBRAUCServer) controllerInfo(c *gin.Context) {
}
}
// swagger:operation GET /reboot startReboot
// swagger:operation POST /reboot startReboot
//
// Reboot the system
//
@ -219,7 +231,7 @@ func (srv *SEBRAUCServer) controllerInfo(c *gin.Context) {
// schema:
// $ref: "#/definitions/Error"
func (srv *SEBRAUCServer) controllerReboot(c *gin.Context) {
go util.Reboot(5 * time.Second)
go util.Reboot(srv.config.Commands.Reboot, 5*time.Second)
writeStatus(c, true, "System is rebooting")
}

View file

@ -13,7 +13,7 @@ var swaggerHtml []byte
//go:embed swagger.yaml
var swaggerYaml []byte
func Register(r *gin.Engine) {
func Register(r gin.IRouter) {
swg := r.Group("/api/swagger", middleware.Cache)
swg.GET("/", func(c *gin.Context) {

View file

@ -166,7 +166,7 @@ paths:
schema:
$ref: "#/definitions/Error"
/reboot:
get:
post:
description: Reboot the system
operationId: startReboot
produces:

View file

@ -11,6 +11,15 @@ import (
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
)
type Sysinfo struct {
cmdRaucStatus string
releaseFile string
hostnameFile string
uptimeFile string
rexpName *regexp.Regexp
rexpVersion *regexp.Regexp
}
type raucInfo struct {
Compatible string `json:"compatible"`
Variant string `json:"variant"`
@ -34,11 +43,27 @@ type osRelease struct {
OsVersion string `json:"os_version"`
}
var (
rexpOsName = regexp.MustCompile(`(?m)^NAME="(.+)"`)
rexpOsVersion = regexp.MustCompile(`(?m)^VERSION="(.+)"`)
rexpUptime = regexp.MustCompile(`^\d+`)
)
var rexpUptime = regexp.MustCompile(`^\d+`)
func New(cmdRaucStatus string, releaseFile string, nameKey string, versionKey string,
hostnameFile string, uptimeFile string) *Sysinfo {
return &Sysinfo{
cmdRaucStatus: cmdRaucStatus,
releaseFile: releaseFile,
hostnameFile: hostnameFile,
uptimeFile: uptimeFile,
rexpName: regexp.MustCompile(
`(?m)^` + regexp.QuoteMeta(nameKey) + `="(.+)"`),
rexpVersion: regexp.MustCompile(
`(?m)^` + regexp.QuoteMeta(versionKey) + `="(.+)"`),
}
}
func Default(cmdRaucStatus string) *Sysinfo {
return New(cmdRaucStatus, "/etc/os-release", "NAME", "VERSION",
"/etc/hostname", "/proc/uptime")
}
func parseRaucInfo(raucInfoJson []byte) (raucInfo, error) {
res := raucInfo{}
@ -46,14 +71,14 @@ func parseRaucInfo(raucInfoJson []byte) (raucInfo, error) {
return res, err
}
func parseOsRelease(osReleaseFile string) (osRelease, error) {
osReleaseTxt, err := os.ReadFile(osReleaseFile)
func (s *Sysinfo) parseOsRelease() (osRelease, error) {
osReleaseTxt, err := os.ReadFile(s.releaseFile)
if err != nil {
return osRelease{}, err
}
nameMatch := rexpOsName.FindSubmatch(osReleaseTxt)
versionMatch := rexpOsVersion.FindSubmatch(osReleaseTxt)
nameMatch := s.rexpName.FindSubmatch(osReleaseTxt)
versionMatch := s.rexpVersion.FindSubmatch(osReleaseTxt)
name := ""
if nameMatch != nil {
@ -116,8 +141,8 @@ func mapSysinfo(rinf raucInfo, osr osRelease, uptime int,
}
}
func getUptime() (int, error) {
uptimeRaw, err := os.ReadFile("/proc/uptime")
func (s *Sysinfo) getUptime() (int, error) {
uptimeRaw, err := os.ReadFile(s.uptimeFile)
if err != nil {
return 0, err
}
@ -126,16 +151,16 @@ func getUptime() (int, error) {
return strconv.Atoi(string(uptimeChars))
}
func getHostname() string {
hostname, err := os.ReadFile("/etc/hostname")
func (s *Sysinfo) getHostname() string {
hostname, err := os.ReadFile(s.hostnameFile)
if err != nil {
return ""
}
return strings.TrimSpace(string(hostname))
}
func GetSysinfo() (model.SystemInfo, error) {
cmd := util.CommandFromString(util.RaucCmd + " status --output-format=json")
func (s *Sysinfo) GetSysinfo() (model.SystemInfo, error) {
cmd := util.CommandFromString(s.cmdRaucStatus)
rinfJson, err := cmd.Output()
if err != nil {
return model.SystemInfo{}, err
@ -146,17 +171,17 @@ func GetSysinfo() (model.SystemInfo, error) {
return model.SystemInfo{}, err
}
osinf, err := parseOsRelease("/etc/os-release")
osinf, err := s.parseOsRelease()
if err != nil {
return model.SystemInfo{}, err
}
uptime, err := getUptime()
uptime, err := s.getUptime()
if err != nil {
return model.SystemInfo{}, err
}
hostname := getHostname()
hostname := s.getHostname()
return mapSysinfo(rinf, osinf, uptime, hostname), nil
}

View file

@ -5,6 +5,7 @@ import (
"testing"
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd"
"code.thetadev.de/TSGRain/SEBRAUC/src/model"
"github.com/stretchr/testify/assert"
)
@ -83,7 +84,10 @@ func TestParseOsRelease(t *testing.T) {
testfiles := fixtures.GetTestfilesDir()
osReleaseFile := filepath.Join(testfiles, "os-release")
osRel, err := parseOsRelease(osReleaseFile)
si := New(testcmd.RaucStatus, osReleaseFile, "NAME", "VERSION",
"/etc/hostname", "/proc/uptime")
osRel, err := si.parseOsRelease()
if err != nil {
panic(err)
}
@ -111,7 +115,9 @@ func TestGetFSNameFromBootname(t *testing.T) {
}
func TestGetSysinfo(t *testing.T) {
sysinfo, err := GetSysinfo()
si := Default(testcmd.RaucStatus)
sysinfo, err := si.GetSysinfo()
if err != nil {
panic(err)
}

View file

@ -1,13 +0,0 @@
//go:build prod
// +build prod
package util
import "code.thetadev.de/TSGRain/SEBRAUC/src/util/mode"
const (
RebootCmd = "shutdown -r 0"
RaucCmd = "rauc"
appmode = mode.Prod
)

View file

@ -1,13 +0,0 @@
//go:build !prod
// +build !prod
package util
import "code.thetadev.de/TSGRain/SEBRAUC/src/util/mode"
const (
RebootCmd = "touch /tmp/sebrauc_reboot_test"
RaucCmd = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock"
appmode = mode.Dev
)

View file

@ -13,6 +13,10 @@ const (
var currentMode = Dev
func init() {
Set(appmode)
}
func Set(newMode string) {
currentMode = newMode
updateGinMode()

View file

@ -0,0 +1,6 @@
//go:build !prod
// +build !prod
package mode
const appmode = Dev

View file

@ -0,0 +1,6 @@
//go:build prod
// +build prod
package mode
const appmode = Prod

View file

@ -25,8 +25,8 @@ func CreateDirIfNotExists(dirpath string) error {
return nil
}
func GetTmpdir() (string, error) {
tmpdir := filepath.Join(os.TempDir(), tmpdirName)
func GetTmpdir(root string) (string, error) {
tmpdir := filepath.Join(root, tmpdirName)
err := CreateDirIfNotExists(tmpdir)
return tmpdir, err
}
@ -41,8 +41,8 @@ func CommandFromString(cmdString string) *exec.Cmd {
return exec.Command(parts[0], parts[1:]...)
}
func Reboot(t time.Duration) {
func Reboot(rebootCmd string, t time.Duration) {
time.Sleep(t)
cmd := CommandFromString(RebootCmd)
cmd := CommandFromString(rebootCmd)
_ = cmd.Run()
}

View file

@ -6,6 +6,7 @@ import (
"testing"
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd"
"github.com/stretchr/testify/assert"
)
@ -41,7 +42,7 @@ func TestDoesFileExist(t *testing.T) {
}
func TestTmpdir(t *testing.T) {
td, err := GetTmpdir()
td, err := GetTmpdir("/tmp")
if err != nil {
panic(err)
}
@ -103,7 +104,7 @@ func TestReboot(t *testing.T) {
testfile := "/tmp/sebrauc_reboot_test"
_ = os.Remove(testfile)
Reboot(0)
Reboot(testcmd.Reboot, 0)
assert.FileExists(t, testfile)
}

View file

@ -1,13 +1,7 @@
package util
import "code.thetadev.de/TSGRain/SEBRAUC/src/util/mode"
var version = "dev"
func init() {
mode.Set(appmode)
}
func Version() string {
return version
}

View file

@ -295,7 +295,7 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
baseOptions = configuration.baseOptions
}
const localVarRequestOptions = {method: "GET", ...baseOptions, ...options}
const localVarRequestOptions = {method: "POST", ...baseOptions, ...options}
const localVarHeaderParameter = {} as any
const localVarQueryParameter = {} as any

View file

@ -9,7 +9,6 @@ import (
"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"
)
@ -34,15 +33,15 @@ func distFS() fs.FS {
return subFS(assets, distDir)
}
func Register(r *gin.Engine) {
func Register(r gin.IRouter) {
indexHandler := getIndexHandler()
ui := r.Group("/", ginzip.New(ginzip.DefaultOptions()), middleware.Cache)
uiAssets := r.Group("/assets", middleware.Cache)
ui.GET("/", indexHandler)
ui.GET("/index.html", indexHandler)
r.GET("/", indexHandler)
r.GET("/index.html", indexHandler)
ui.StaticFS("/assets", http.FS(subFS(distFS(), "assets")))
uiAssets.StaticFS("/", http.FS(subFS(distFS(), "assets")))
}
func getIndexHandler() gin.HandlerFunc {

View file

@ -19,5 +19,5 @@ func TestUI(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "SEBRAUC")
assert.NotEmpty(t, w.Header().Get("Cache-Control"))
assert.Empty(t, w.Header().Get("Cache-Control"))
}