diff --git a/Makefile b/Makefile index 469c98f..f1437d1 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ daemon-compile: daemon-compile-armhf daemon-compile-x64-lin daemon-compile-x64-w daemon-codestyle: go fmt ./... go vet ./... - gocyclo -over 3 cmd internal + gocyclo -over 4 cmd internal daemon-unittests: mkdir -p reports diff --git a/OliveTin.proto b/OliveTin.proto index 91dab54..4eea920 100644 --- a/OliveTin.proto +++ b/OliveTin.proto @@ -4,7 +4,7 @@ option go_package = "gen/grpc"; import "google/api/annotations.proto"; -message ActionButton { +message Action { string id = 1; string title = 2; string icon = 3; @@ -15,7 +15,7 @@ message ActionButton { message ActionArgument { string name = 1; - string label = 2; + string title = 2; string type = 3; string defaultValue = 4; @@ -24,18 +24,32 @@ message ActionArgument { message ActionArgumentChoice { string value = 1; - string label = 2; + string title = 2; } -message GetButtonsResponse { +message Entity { string title = 1; - repeated ActionButton actions = 2; + string icon = 2; + repeated Action actions = 3; } -message GetButtonsRequest {} +message GetDashboardComponentsResponse { + string title = 1; + repeated Action actions = 2; + repeated Entity entities = 3; +} + +message GetDashboardComponentsRequest {} message StartActionRequest { string actionName = 1; + + repeated StartActionArgument arguments = 2; +} + +message StartActionArgument { + string name = 1; + string value = 2; } message StartActionResponse { @@ -53,22 +67,34 @@ message LogEntry { int32 exitCode = 6; string user = 7; string userClass = 8; + string actionIcon = 9; } message GetLogsResponse { repeated LogEntry logs = 1; } +message ValidateArgumentTypeRequest { + string value = 1; + string type = 2; +} + +message ValidateArgumentTypeResponse { + bool valid = 1; + string description = 2; +} + service OliveTinApi { - rpc GetButtons(GetButtonsRequest) returns (GetButtonsResponse) { + rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) { option (google.api.http) = { - get: "/api/GetButtons" + get: "/api/GetDashboardComponents" }; } rpc StartAction(StartActionRequest) returns (StartActionResponse) { option (google.api.http) = { - get: "/api/StartAction" + post: "/api/StartAction" + body: "*" }; } @@ -77,4 +103,11 @@ service OliveTinApi { get: "/api/GetLogs" }; } + + rpc ValidateArgumentType(ValidateArgumentTypeRequest) returns (ValidateArgumentTypeResponse) { + option (google.api.http) = { + post: "/api/ValidateArgumentType" + body: "*" + }; + } } diff --git a/cmd/OliveTin/main.go b/cmd/OliveTin/main.go index 8fa80a7..4086dff 100644 --- a/cmd/OliveTin/main.go +++ b/cmd/OliveTin/main.go @@ -44,8 +44,6 @@ func init() { cfg = config.DefaultConfig() - reloadConfig() - viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { if e.Op == fsnotify.Write { @@ -54,6 +52,9 @@ func init() { reloadConfig() } }) + + reloadConfig() + log.Info("Init complete") } func reloadConfig() { @@ -62,7 +63,7 @@ func reloadConfig() { os.Exit(1) } - config.Sanitize(cfg); + config.Sanitize(cfg) } func main() { diff --git a/internal/acl/acl.go b/internal/acl/acl.go index f01e909..fade75e 100644 --- a/internal/acl/acl.go +++ b/internal/acl/acl.go @@ -10,7 +10,7 @@ type User struct { Username string } -func IsAllowedExec(cfg *config.Config, user *User, action *config.ActionButton) bool { +func IsAllowedExec(cfg *config.Config, user *User, action *config.Action) bool { canExec := cfg.DefaultPermissions.Exec log.WithFields(log.Fields{ @@ -40,7 +40,7 @@ func IsAllowedExec(cfg *config.Config, user *User, action *config.ActionButton) return canExec } -func IsAllowedView(cfg *config.Config, user *User, action *config.ActionButton) bool { +func IsAllowedView(cfg *config.Config, user *User, action *config.Action) bool { canView := cfg.DefaultPermissions.View log.WithFields(log.Fields{ diff --git a/internal/config/config.go b/internal/config/config.go index 8898be1..6965544 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,8 +2,7 @@ package config import () -// ActionButton represents a button that is shown in the webui. -type ActionButton struct { +type Action struct { ID string Title string Icon string @@ -16,7 +15,7 @@ type ActionButton struct { type ActionArgument struct { Name string - Label string + Title string Type string Default string Choices []ActionArgumentChoice @@ -24,16 +23,16 @@ type ActionArgument struct { type ActionArgumentChoice struct { Value string - Label string + Title string } // Entity represents a "thing" that can have multiple actions associated with it. // for example, a media player with a start and stop action. type Entity struct { - Title string - Icon string - ActionButtons []ActionButton `mapstructure:"actions"` - CSS map[string]string + Title string + Icon string + Actions []Action `mapstructure:"actions"` + CSS map[string]string } type PermissionsEntry struct { @@ -63,9 +62,10 @@ type Config struct { ListenAddressGrpcActions string ExternalRestAddress string LogLevel string - ActionButtons []ActionButton `mapstructure:"actions"` - Entities []Entity `mapstructure:"entities"` + Actions []Action `mapstructure:"actions"` + Entities []Entity `mapstructure:"entities"` CheckForUpdates bool + ShowNewVersions bool Usergroups []UserGroup DefaultPermissions DefaultPermissions } @@ -81,6 +81,7 @@ func DefaultConfig() *Config { config.ListenAddressWebUI = "localhost:1340" config.LogLevel = "INFO" config.CheckForUpdates = true + config.ShowNewVersions = true config.DefaultPermissions.Exec = true config.DefaultPermissions.View = true diff --git a/internal/config/config_helpers.go b/internal/config/config_helpers.go new file mode 100644 index 0000000..581ce65 --- /dev/null +++ b/internal/config/config_helpers.go @@ -0,0 +1,11 @@ +package config + +func (cfg *Config) FindAction(actionTitle string) *Action { + for _, action := range cfg.Actions { + if action.Title == actionTitle { + return &action + } + } + + return nil +} diff --git a/internal/grpcapi/emoji.go b/internal/config/emoji.go similarity index 93% rename from internal/grpcapi/emoji.go rename to internal/config/emoji.go index c31b29f..ba33c4a 100644 --- a/internal/grpcapi/emoji.go +++ b/internal/config/emoji.go @@ -1,4 +1,4 @@ -package grpcapi +package config var emojis = map[string]string{ "poop": "💩", diff --git a/internal/grpcapi/emoji_test.go b/internal/config/emoji_test.go similarity index 94% rename from internal/grpcapi/emoji_test.go rename to internal/config/emoji_test.go index 2b5dafc..5737c1c 100644 --- a/internal/grpcapi/emoji_test.go +++ b/internal/config/emoji_test.go @@ -1,4 +1,4 @@ -package grpcapi +package config import ( "github.com/stretchr/testify/assert" diff --git a/internal/config/sanitize.go b/internal/config/sanitize.go index cb6c6db..adfa368 100644 --- a/internal/config/sanitize.go +++ b/internal/config/sanitize.go @@ -5,28 +5,50 @@ import ( ) func Sanitize(cfg *Config) { - sanitizeLogLevel(cfg); + sanitizeLogLevel(cfg) - for _, action := range cfg.ActionButtons { - sanitizeAction(action) + //log.Infof("cfg %p", cfg) + + for idx, _ := range cfg.Actions { + sanitizeAction(&cfg.Actions[idx]) } } func sanitizeLogLevel(cfg *Config) { if logLevel, err := log.ParseLevel(cfg.LogLevel); err == nil { - log.Info("lvl", logLevel) + log.Info("Setting log level to ", logLevel) log.SetLevel(logLevel) } } -func sanitizeAction(action ActionButton) { - for _, argument := range action.Arguments { - sanitizeActionArgument(argument) +func sanitizeAction(action *Action) { + if action.Timeout < 3 { + action.Timeout = 3 + } + + action.Icon = lookupHTMLIcon(action.Icon) + + for idx, _ := range action.Arguments { + sanitizeActionArgument(&action.Arguments[idx]) } } -func sanitizeActionArgument(arg ActionArgument) { - log.Info("Sanitize AA") - arg.Label = "foo" - arg.Name = "blat" +func sanitizeActionArgument(arg *ActionArgument) { + if arg.Title == "" { + arg.Title = arg.Name + } + + sanitizeActionArgumentNoType(arg) + + // TODO Validate the default against the type checker, but this creates a + // import loop +} + +func sanitizeActionArgumentNoType(arg *ActionArgument) { + if len(arg.Choices) == 0 && arg.Type == "" { + log.WithFields(log.Fields{ + "arg": arg.Name, + }).Warn("Argument type isn't set, will default to 'ascii' but this may not be safe. You should set a type specifically.") + arg.Type = "ascii" + } } diff --git a/internal/executor/executor.go b/internal/executor/executor.go index e8b0b2c..25954ee 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -9,99 +9,286 @@ import ( "context" "errors" "os/exec" + "regexp" + "strings" "time" ) +var ( + typecheckRegex = map[string]string{ + "very_dangerous_raw_string": "", + "int": "^[\\d]+$", + "ascii": "^[a-zA-Z0-9]+$", + "ascii_identifier": "^[a-zA-Z0-9\\-\\.\\_]+$", + "ascii_sentence": "^[a-zA-Z0-9 \\,\\.]+$", + } +) + type InternalLogEntry struct { - Datetime string - Content string - Stdout string - Stderr string - TimedOut bool - ExitCode int32 + Datetime string + Stdout string + Stderr string + TimedOut bool + ExitCode int32 + + /* + The following two properties are obviously on Action normally, but it's useful + that logs are lightweight (so we don't need to have an action associated to + logs, etc. Therefore, we duplicate those values here. + */ ActionTitle string + ActionIcon string +} + +type ExecutionRequest struct { + ActionName string + Arguments map[string]string + action *config.Action + Cfg *config.Config + User *acl.User + logEntry *InternalLogEntry + finalParsedCommand string +} + +type ExecutorStep interface { + Exec(*ExecutionRequest) bool } type Executor struct { Logs []InternalLogEntry + + chainOfCommand []ExecutorStep } -// ExecAction executes an action. -func (e *Executor) ExecAction(cfg *config.Config, user *acl.User, actualAction *config.ActionButton) *pb.StartActionResponse { - log.WithFields(log.Fields{ - "actionName": actualAction.Title, - }).Infof("StartAction") +func DefaultExecutor() *Executor { + e := Executor{} + e.chainOfCommand = []ExecutorStep{ + StepFindAction{}, + StepAclCheck{}, + StepParseArgs{}, + StepLogStart{}, + StepExec{}, + StepLogFinish{}, + } - res := execAction(cfg, actualAction) + return &e +} - e.Logs = append(e.Logs, *res) +type StepFindAction struct{} + +func (s StepFindAction) Exec(req *ExecutionRequest) bool { + actualAction := req.Cfg.FindAction(req.ActionName) + + if actualAction == nil { + log.WithFields(log.Fields{ + "actionName": req.ActionName, + }).Warnf("Action not found") + + req.logEntry.Stderr = "Action not found" + req.logEntry.ExitCode = -1337 + + return false + } + + req.action = actualAction + req.logEntry.ActionIcon = actualAction.Icon + + return true +} + +type StepAclCheck struct{} + +func (s StepAclCheck) Exec(req *ExecutionRequest) bool { + return acl.IsAllowedExec(req.Cfg, req.User, req.action) +} + +// ExecRequest processes an ExecutionRequest +func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse { + req.logEntry = &InternalLogEntry{ + Datetime: time.Now().Format("2006-01-02 15:04:05"), + ActionTitle: req.ActionName, + } + + for _, step := range e.chainOfCommand { + if !step.Exec(req) { + break + } + } + + e.Logs = append(e.Logs, *req.logEntry) return &pb.StartActionResponse{ LogEntry: &pb.LogEntry{ - ActionTitle: actualAction.Title, - TimedOut: res.TimedOut, - Stderr: res.Stderr, - Stdout: res.Stdout, - ExitCode: res.ExitCode, + ActionTitle: req.logEntry.ActionTitle, + ActionIcon: req.logEntry.ActionIcon, + Datetime: req.logEntry.Datetime, + Stderr: req.logEntry.Stderr, + Stdout: req.logEntry.Stdout, + TimedOut: req.logEntry.TimedOut, + ExitCode: req.logEntry.ExitCode, }, } } -func execAction(cfg *config.Config, actualAction *config.ActionButton) *InternalLogEntry { - res := &InternalLogEntry{ - Datetime: time.Now().Format("2006-01-02 15:04:05"), - TimedOut: false, - ActionTitle: actualAction.Title, +type StepLogStart struct{} + +func (e StepLogStart) Exec(req *ExecutionRequest) bool { + log.WithFields(log.Fields{ + "title": req.action.Title, + "timeout": req.action.Timeout, + }).Infof("Action starting") + + return true +} + +type StepLogFinish struct{} + +func (e StepLogFinish) Exec(req *ExecutionRequest) bool { + log.WithFields(log.Fields{ + "title": req.action.Title, + "stdout": req.logEntry.Stdout, + "stderr": req.logEntry.Stderr, + "timedOut": req.logEntry.TimedOut, + "exit": req.logEntry.ExitCode, + }).Infof("Action finished") + + return true +} + +type StepParseArgs struct{} + +func (e StepParseArgs) Exec(req *ExecutionRequest) bool { + var err error + + req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action) + + if err != nil { + req.logEntry.ExitCode = -1337 + req.logEntry.Stderr = "" + req.logEntry.Stdout = err.Error() + + log.Warnf(err.Error()) + + return false } - log.WithFields(log.Fields{ - "title": actualAction.Title, - "timeout": actualAction.Timeout, - }).Infof("Found action") + return true +} - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(actualAction.Timeout)*time.Second) +type StepExec struct{} + +func (e StepExec) Exec(req *ExecutionRequest) bool { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, "sh", "-c", actualAction.Shell) + cmd := exec.CommandContext(ctx, "sh", "-c", req.finalParsedCommand) stdout, stderr := cmd.Output() - res.ExitCode = int32(cmd.ProcessState.ExitCode()) - res.Stdout = string(stdout) - - if stderr == nil { - res.Stderr = "" - } else { - res.Stderr = stderr.Error() + if stderr != nil { + req.logEntry.Stderr = stderr.Error() } if ctx.Err() == context.DeadlineExceeded { - res.TimedOut = true + req.logEntry.TimedOut = true + } + + req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode()) + req.logEntry.Stdout = string(stdout) + + return true +} + +func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action) (string, error) { + log.WithFields(log.Fields{ + "cmd": rawShellCommand, + }).Infof("Before Parse Args") + + r := regexp.MustCompile("{{ *?([a-z]+?) *?}}") + matches := r.FindAllStringSubmatch(rawShellCommand, -1) + + for _, match := range matches { + argValue, argProvided := values[match[1]] + + if !argProvided { + log.Infof("%v", values) + return "", errors.New("Required arg not provided: " + match[1]) + } + + err := typecheckActionArgument(match[1], argValue, action) + + if err != nil { + return "", err + } + + log.WithFields(log.Fields{ + "name": match[1], + "value": argValue, + }).Debugf("Arg assigned") + + rawShellCommand = strings.Replace(rawShellCommand, match[0], argValue, -1) } log.WithFields(log.Fields{ - "stdout": res.Stdout, - "stderr": res.Stderr, - "timedOut": res.TimedOut, - "exit": res.ExitCode, - }).Infof("Finished command.") + "cmd": rawShellCommand, + }).Infof("After Parse Args") - return res + return rawShellCommand, nil } -func sanitizeAction(action *config.ActionButton) { - if action.Timeout < 3 { - action.Timeout = 3 +func typecheckActionArgument(name string, value string, action *config.Action) error { + arg := findArg(name, action) + + if arg == nil { + return errors.New("Action arg not defined: " + name) } + + if len(arg.Choices) > 0 { + return typecheckChoice(value, arg) + } + + return TypeSafetyCheck(name, value, arg.Type) } -func FindAction(cfg *config.Config, actionTitle string) (*config.ActionButton, error) { - for _, action := range cfg.ActionButtons { - if action.Title == actionTitle { - sanitizeAction(&action) - - return &action, nil +func typecheckChoice(value string, arg *config.ActionArgument) error { + for _, choice := range arg.Choices { + if value == choice.Value { + return nil } } - return nil, errors.New("Action not found") + return errors.New("Arg value is not one of the predefined choices") +} + +func TypeSafetyCheck(name string, value string, typ string) error { + pattern, found := typecheckRegex[typ] + + log.Infof("%v %v", pattern, typ) + + if !found { + return errors.New("Arg type not implemented " + typ) + } + + matches, _ := regexp.MatchString(pattern, value) + + if !matches { + log.WithFields(log.Fields{ + "name": name, + "type": typ, + "value": value, + }).Warn("Arg type check safety failure") + + return errors.New("Invalid argument, doesn't match " + typ) + } + + return nil +} + +func findArg(name string, action *config.Action) *config.ActionArgument { + for _, arg := range action.Arguments { + if arg.Name == name { + return &arg + } + } + + return nil } diff --git a/internal/grpcapi/grpcApi.go b/internal/grpcapi/grpcApi.go index ff78e24..c037dcb 100644 --- a/internal/grpcapi/grpcApi.go +++ b/internal/grpcapi/grpcApi.go @@ -14,7 +14,7 @@ import ( var ( cfg *config.Config - ex = executor.Executor{} + ex = executor.DefaultExecutor() ) type oliveTinAPI struct { @@ -22,36 +22,34 @@ type oliveTinAPI struct { } func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) { - actualAction, err := executor.FindAction(cfg, req.ActionName) + args := make(map[string]string) - if err != nil { - log.Errorf("Error finding action %s, %s", err, req.ActionName) + log.Debugf("SA %v", req) - return &pb.StartActionResponse{ - LogEntry: nil, - }, nil + for _, arg := range req.Arguments { + args[arg.Name] = arg.Value } - user := acl.UserFromContext(ctx) - - if !acl.IsAllowedExec(cfg, user, actualAction) { - return &pb.StartActionResponse{}, nil - + execReq := executor.ExecutionRequest{ + ActionName: req.ActionName, + Arguments: args, + User: acl.UserFromContext(ctx), + Cfg: cfg, } - return ex.ExecAction(cfg, acl.UserFromContext(ctx), actualAction), nil + return ex.ExecRequest(&execReq), nil } -func (api *oliveTinAPI) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (*pb.GetButtonsResponse, error) { +func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) { user := acl.UserFromContext(ctx) - res := actionButtonsCfgToPb(cfg.ActionButtons, user) + res := actionsCfgToPb(cfg.Actions, user) if len(res.Actions) == 0 { log.Warn("Zero actions found - check that you have some actions defined, with a view permission") } - log.Debugf("getButtons: %v", res) + log.Debugf("GetDashboardComponents: %v", res) return res, nil } @@ -64,6 +62,7 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge for _, logEntry := range ex.Logs { ret.Logs = append(ret.Logs, &pb.LogEntry{ ActionTitle: logEntry.ActionTitle, + ActionIcon: logEntry.ActionIcon, Datetime: logEntry.Datetime, Stdout: logEntry.Stdout, Stderr: logEntry.Stderr, @@ -75,6 +74,25 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge return ret, nil } +/* +This function is ONLY a helper for the UI - the arguments are validated properly +on the StartAction -> Executor chain. This is here basically to provide helpful +error messages more quickly before starting the action. +*/ +func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *pb.ValidateArgumentTypeRequest) (*pb.ValidateArgumentTypeResponse, error) { + err := executor.TypeSafetyCheck("", req.Value, req.Type) + desc := "" + + if err != nil { + desc = err.Error() + } + + return &pb.ValidateArgumentTypeResponse{ + Valid: err == nil, + Description: desc, + }, nil +} + // Start will start the GRPC API. func Start(globalConfig *config.Config) { cfg = globalConfig diff --git a/internal/grpcapi/grpcApiButtons.go b/internal/grpcapi/grpcApiActions.go similarity index 70% rename from internal/grpcapi/grpcApiButtons.go rename to internal/grpcapi/grpcApiActions.go index 0a3aba3..d534182 100644 --- a/internal/grpcapi/grpcApiButtons.go +++ b/internal/grpcapi/grpcApiActions.go @@ -8,33 +8,33 @@ import ( config "github.com/jamesread/OliveTin/internal/config" ) -func actionButtonsCfgToPb(cfgActionButtons []config.ActionButton, user *acl.User) (*pb.GetButtonsResponse) { - res := &pb.GetButtonsResponse{} +func actionsCfgToPb(cfgActions []config.Action, user *acl.User) *pb.GetDashboardComponentsResponse { + res := &pb.GetDashboardComponentsResponse{} - for _, action := range cfgActionButtons { + for _, action := range cfgActions { if !acl.IsAllowedView(cfg, user, &action) { continue } - btn := buildButton(action, user) + btn := actionCfgToPb(action, user) res.Actions = append(res.Actions, btn) } return res } -func buildButton(action config.ActionButton, user *acl.User) *pb.ActionButton { - btn := pb.ActionButton{ +func actionCfgToPb(action config.Action, user *acl.User) *pb.Action { + btn := pb.Action{ Id: fmt.Sprintf("%x", md5.Sum([]byte(action.Title))), Title: action.Title, - Icon: lookupHTMLIcon(action.Icon), + Icon: action.Icon, CanExec: acl.IsAllowedExec(cfg, user, &action), } for _, cfgArg := range action.Arguments { pbArg := pb.ActionArgument{ Name: cfgArg.Name, - Label: cfgArg.Label, + Title: cfgArg.Title, Type: cfgArg.Type, DefaultValue: cfgArg.Default, Choices: buildChoices(cfgArg.Choices), @@ -52,7 +52,7 @@ func buildChoices(choices []config.ActionArgumentChoice) []*pb.ActionArgumentCho for _, cfgChoice := range choices { pbChoice := pb.ActionArgumentChoice{ Value: cfgChoice.Value, - Label: cfgChoice.Label, + Title: cfgChoice.Title, } ret = append(ret, &pbChoice) diff --git a/internal/grpcapi/grpcApi_test.go b/internal/grpcapi/grpcApi_test.go index fcf0c43..2024ae3 100644 --- a/internal/grpcapi/grpcApi_test.go +++ b/internal/grpcapi/grpcApi_test.go @@ -52,19 +52,19 @@ func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*gr return conn, client } -func TestGetButtonsAndStart(t *testing.T) { +func TestGetActionsAndStart(t *testing.T) { cfg = config.DefaultConfig() - btn1 := config.ActionButton{} + btn1 := config.Action{} btn1.Title = "blat" btn1.Shell = "echo 'test'" - cfg.ActionButtons = append(cfg.ActionButtons, btn1) + cfg.Actions = append(cfg.Actions, btn1) conn, client := getNewTestServerAndClient(t, cfg) - respGb, err := client.GetButtons(context.Background(), &pb.GetButtonsRequest{}) + respGb, err := client.GetDashboardComponents(context.Background(), &pb.GetDashboardComponentsRequest{}) if err != nil { - t.Errorf("GetButtons: %v", err) + t.Errorf("GetDashboardComponentsRequest: %v", err) } assert.Equal(t, true, true, "sayHello Failed") diff --git a/internal/httpservers/webuiServer.go b/internal/httpservers/webuiServer.go index 4824660..f6a2985 100644 --- a/internal/httpservers/webuiServer.go +++ b/internal/httpservers/webuiServer.go @@ -8,12 +8,16 @@ import ( "os" config "github.com/jamesread/OliveTin/internal/config" + updatecheck "github.com/jamesread/OliveTin/internal/updatecheck" ) type webUISettings struct { - Rest string - ThemeName string - HideNavigation bool + Rest string + ThemeName string + HideNavigation bool + AvailableVersion string + CurrentVersion string + ShowNewVersions bool } func findWebuiDir() string { @@ -43,9 +47,12 @@ func generateWebUISettings(w http.ResponseWriter, r *http.Request) { } jsonRet, _ := json.Marshal(webUISettings{ - Rest: restAddress + "/api/", - ThemeName: cfg.ThemeName, - HideNavigation: cfg.HideNavigation, + Rest: restAddress + "/api/", + ThemeName: cfg.ThemeName, + HideNavigation: cfg.HideNavigation, + AvailableVersion: updatecheck.AvailableVersion, + CurrentVersion: updatecheck.CurrentVersion, + ShowNewVersions: cfg.ShowNewVersions, }) w.Write([]byte(jsonRet)) diff --git a/internal/updatecheck/updateCheck.go b/internal/updatecheck/updateCheck.go index f4915ed..ef3c443 100644 --- a/internal/updatecheck/updateCheck.go +++ b/internal/updatecheck/updateCheck.go @@ -21,6 +21,9 @@ type updateRequest struct { MachineID string } +var AvailableVersion = "none" +var CurrentVersion = "?" + func machineID() string { v, err := machineid.ProtectedID("OliveTin") @@ -40,6 +43,8 @@ func StartUpdateChecker(currentVersion string, currentCommit string, cfg *config return } + CurrentVersion = currentVersion + payload := updateRequest{ CurrentVersion: currentVersion, CurrentCommit: currentCommit, @@ -89,9 +94,9 @@ func actualCheckForUpdate(payload updateRequest) { return } - newVersion := doRequest(jsonUpdateRequest) + AvailableVersion = doRequest(jsonUpdateRequest) log.WithFields(log.Fields{ - "NewVersion": newVersion, + "NewVersion": AvailableVersion, }).Infof("Update check complete") } diff --git a/webui/index.html b/webui/index.html index fab9ab6..20aee26 100644 --- a/webui/index.html +++ b/webui/index.html @@ -23,6 +23,7 @@
OliveTin
Documentation | - Raise an issue on GitHub + Raise an issue on GitHub | + Version: ?
+ ? + +Untitled Button
-+?
+ ? ++