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 4c59617..4eea920 100644 --- a/OliveTin.proto +++ b/OliveTin.proto @@ -4,22 +4,52 @@ option go_package = "gen/grpc"; import "google/api/annotations.proto"; -message ActionButton { +message Action { string id = 1; string title = 2; string icon = 3; bool canExec = 4; + + repeated ActionArgument arguments = 5; } -message GetButtonsResponse { +message ActionArgument { + string name = 1; + string title = 2; + string type = 3; + string defaultValue = 4; + + repeated ActionArgumentChoice choices = 5; +} + +message ActionArgumentChoice { + string value = 1; + string title = 2; +} + +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 { @@ -37,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: "*" }; } @@ -61,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 e7c92fd..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,9 +63,7 @@ func reloadConfig() { os.Exit(1) } - if logLevel, err := log.ParseLevel(cfg.LogLevel); err == nil { - log.SetLevel(logLevel) - } + 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 01cd8a3..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 @@ -11,15 +10,29 @@ type ActionButton struct { CSS map[string]string `mapstructure:"omitempty"` Timeout int Permissions []PermissionsEntry + Arguments []ActionArgument +} + +type ActionArgument struct { + Name string + Title string + Type string + Default string + Choices []ActionArgumentChoice +} + +type ActionArgumentChoice struct { + Value 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 { @@ -49,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 } @@ -67,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 new file mode 100644 index 0000000..adfa368 --- /dev/null +++ b/internal/config/sanitize.go @@ -0,0 +1,54 @@ +package config + +import ( + log "github.com/sirupsen/logrus" +) + +func Sanitize(cfg *Config) { + sanitizeLogLevel(cfg) + + //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("Setting log level to ", logLevel) + log.SetLevel(logLevel) + } +} + +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) { + 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 51270db..c037dcb 100644 --- a/internal/grpcapi/grpcApi.go +++ b/internal/grpcapi/grpcApi.go @@ -2,8 +2,6 @@ package grpcapi import ( ctx "context" - "crypto/md5" - "fmt" pb "github.com/jamesread/OliveTin/gen/grpc" log "github.com/sirupsen/logrus" "google.golang.org/grpc" @@ -16,7 +14,7 @@ import ( var ( cfg *config.Config - ex = executor.Executor{} + ex = executor.DefaultExecutor() ) type oliveTinAPI struct { @@ -24,61 +22,38 @@ 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 } -func actionButtonsCfgToPb(cfgActionButtons []config.ActionButton, user *acl.User) (*pb.GetButtonsResponse) { - res := &pb.GetButtonsResponse{} - - for _, action := range cfgActionButtons { - if !acl.IsAllowedView(cfg, user, &action) { - continue - } - - btn := pb.ActionButton{ - Id: fmt.Sprintf("%x", md5.Sum([]byte(action.Title))), - Title: action.Title, - Icon: lookupHTMLIcon(action.Icon), - CanExec: acl.IsAllowedExec(cfg, user, &action), - } - - res.Actions = append(res.Actions, &btn) - } - - return res -} - func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.GetLogsResponse, error) { ret := &pb.GetLogsResponse{} @@ -87,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, @@ -98,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/grpcApiActions.go b/internal/grpcapi/grpcApiActions.go new file mode 100644 index 0000000..d534182 --- /dev/null +++ b/internal/grpcapi/grpcApiActions.go @@ -0,0 +1,62 @@ +package grpcapi + +import ( + "crypto/md5" + "fmt" + pb "github.com/jamesread/OliveTin/gen/grpc" + acl "github.com/jamesread/OliveTin/internal/acl" + config "github.com/jamesread/OliveTin/internal/config" +) + +func actionsCfgToPb(cfgActions []config.Action, user *acl.User) *pb.GetDashboardComponentsResponse { + res := &pb.GetDashboardComponentsResponse{} + + for _, action := range cfgActions { + if !acl.IsAllowedView(cfg, user, &action) { + continue + } + + btn := actionCfgToPb(action, user) + res.Actions = append(res.Actions, btn) + } + + return res +} + +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: action.Icon, + CanExec: acl.IsAllowedExec(cfg, user, &action), + } + + for _, cfgArg := range action.Arguments { + pbArg := pb.ActionArgument{ + Name: cfgArg.Name, + Title: cfgArg.Title, + Type: cfgArg.Type, + DefaultValue: cfgArg.Default, + Choices: buildChoices(cfgArg.Choices), + } + + btn.Arguments = append(btn.Arguments, &pbArg) + } + + return &btn +} + +func buildChoices(choices []config.ActionArgumentChoice) []*pb.ActionArgumentChoice { + ret := []*pb.ActionArgumentChoice{} + + for _, cfgChoice := range choices { + pbChoice := pb.ActionArgumentChoice{ + Value: cfgChoice.Value, + Title: cfgChoice.Title, + } + + ret = append(ret, &pbChoice) + } + + return ret +} 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
-+?
+ ? ++