Various updates in a ubercommit :-)

This commit is contained in:
jamesread 2021-05-12 10:43:32 +01:00
parent f3d8e5c31e
commit 3d3999de42
24 changed files with 234 additions and 46 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ web/node_modules
**/*.swo **/*.swo
gen/ gen/
go.sum go.sum
OliveTin

View File

@ -3,6 +3,6 @@ default:
grpc: grpc:
protoc -I.:/usr/share/gocode/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/ --go_out=plugins=grpc:gen/grpc/ OliveTin.proto protoc -I.:/usr/share/gocode/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/ --go_out=plugins=grpc:gen/grpc/ OliveTin.proto
protoc -I.:/usr/share/gocode/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/ --grpc-gateway_out=gen/grpc --grpc-gateway_opt paths=source_relative --grpc-gateway_opt generate_unbound_methods=true OliveTin.proto protoc -I.:/usr/share/gocode/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/ --grpc-gateway_out=gen/grpc --grpc-gateway_opt paths=source_relative OliveTin.proto
.PHONY: grpc .PHONY: grpc

View File

@ -18,7 +18,12 @@ message StartActionRequest {
string actionName = 1; string actionName = 1;
} }
message StartActionResponse {} message StartActionResponse {
string stdout = 1;
string stderr = 2;
bool timedOut = 3;
int32 exitCode = 4;
}
service OliveTinApi { service OliveTinApi {
rpc GetButtons(GetButtonsRequest) returns (GetButtonsResponse) { rpc GetButtons(GetButtonsRequest) returns (GetButtonsResponse) {

View File

@ -4,3 +4,11 @@ OliveTin is a web based quick access control panel for running jobs.
For example, it can be used to turn home automation lights on or off, or start For example, it can be used to turn home automation lights on or off, or start
workflows in n8n. workflows in n8n.
## `config.yaml`
```
listenAddressRestActions: :1337 # Listen on all addresses available, port 1337
listenAddressWebUi: :1339
logLevel: "INFO"
```

View File

@ -5,8 +5,10 @@ import (
restApi "github.com/jamesread/OliveTin/pkg/restApi" restApi "github.com/jamesread/OliveTin/pkg/restApi"
grpcApi "github.com/jamesread/OliveTin/pkg/grpcApi" grpcApi "github.com/jamesread/OliveTin/pkg/grpcApi"
webuiServer "github.com/jamesread/OliveTin/pkg/webuiServer"
config "github.com/jamesread/OliveTin/pkg/config" config "github.com/jamesread/OliveTin/pkg/config"
executor "github.com/jamesread/OliveTin/pkg/executor"
"github.com/spf13/viper" "github.com/spf13/viper"
"os" "os"
) )
@ -20,12 +22,14 @@ func init() {
log.SetLevel(log.DebugLevel) // Default to debug, to catch cfg issues log.SetLevel(log.DebugLevel) // Default to debug, to catch cfg issues
viper.AutomaticEnv(); viper.AutomaticEnv();
viper.SetConfigName("config.yaml")
viper.SetConfigType("yaml") viper.SetConfigType("yaml")
viper.AddConfigPath(".") viper.AddConfigPath(".")
viper.AddConfigPath("/etc/olivetin/") viper.AddConfigPath("/etc/OliveTin/")
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
log.Panicf("Config file error %s", err); log.Errorf("Config file error at startup. %s", err);
os.Exit(1);
}; };
cfg = config.DefaultConfig(); cfg = config.DefaultConfig();
@ -42,13 +46,17 @@ func init() {
func main() { func main() {
log.WithFields(log.Fields { log.WithFields(log.Fields {
"listenPortRestActions": cfg.ListenPortRestActions, "listenAddressGrpcActions": cfg.ListenAddressGrpcActions,
"listenPortWebUi": cfg.ListenPortWebUi, "listenAddressRestActions": cfg.ListenAddressRestActions,
"listenAddressWebUi": cfg.ListenAddressWebUi,
}).Info("OliveTin started"); }).Info("OliveTin started");
log.Debugf("%+v", cfg) log.Debugf("%+v", cfg)
go grpcApi.Start() executor.Cfg = cfg;
restApi.Start(); go grpcApi.Start(cfg.ListenAddressGrpcActions, cfg)
go restApi.Start(cfg.ListenAddressRestActions, cfg.ListenAddressGrpcActions, cfg)
webuiServer.Start(cfg.ListenAddressWebUi, cfg.ListenAddressRestActions)
} }

1
go.mod
View File

@ -13,6 +13,7 @@ require (
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/text v0.3.5 // indirect golang.org/x/text v0.3.5 // indirect
golang.org/x/tools v0.1.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20210224155714-063164c882e6 google.golang.org/genproto v0.0.0-20210224155714-063164c882e6
google.golang.org/grpc v1.37.0 google.golang.org/grpc v1.37.0

View File

@ -8,6 +8,7 @@ import (
type ActionButton struct { type ActionButton struct {
Title string Title string
Icon string Icon string
Shell string
Css map[string]string `mapstructure:omitempty` Css map[string]string `mapstructure:omitempty`
} }
@ -19,8 +20,9 @@ type Entity struct {
} }
type Config struct { type Config struct {
ListenPortRestActions int ListenAddressWebUi string
ListenPortWebUi int ListenAddressRestActions string
ListenAddressGrpcActions string
LogLevel string LogLevel string
ActionButtons []ActionButton `mapstructure:"actions"` ActionButtons []ActionButton `mapstructure:"actions"`
Entities []Entity `mapstructure:omitempty` Entities []Entity `mapstructure:omitempty`
@ -28,8 +30,9 @@ type Config struct {
func DefaultConfig() *Config { func DefaultConfig() *Config {
config := Config{}; config := Config{};
config.ListenPortRestActions = 1337; config.ListenAddressWebUi = "0.0.0.0:1337"
config.ListenPortWebUi = 1339; config.ListenAddressRestActions = "0.0.0.0:1338"
config.ListenAddressGrpcActions = "0.0.0.0:1339"
config.LogLevel = "INFO" config.LogLevel = "INFO"
return &config return &config
@ -43,3 +46,4 @@ func (cfg *Config) GetLogLevel() (log.Level) {
default: return log.DebugLevel; default: return log.DebugLevel;
} }
} }

15
pkg/config/config_test.go Normal file
View File

@ -0,0 +1,15 @@
package config
import (
"testing"
log "github.com/sirupsen/logrus"
)
func TestGetLog(t *testing.T) {
c := Config{}
c.LogLevel = ""
if c.GetLogLevel() != log.InfoLevel {
t.Errorf("Info expected")
}
}

65
pkg/executor/executor.go Normal file
View File

@ -0,0 +1,65 @@
package executor
import (
config "github.com/jamesread/OliveTin/pkg/config"
log "github.com/sirupsen/logrus"
pb "github.com/jamesread/OliveTin/gen/grpc"
"errors"
"os/exec"
"context"
"time"
"fmt"
)
var (
Cfg *config.Config;
)
func ExecAction(action string) (*pb.StartActionResponse) {
res := &pb.StartActionResponse{}
res.TimedOut = false;
log.WithFields(log.Fields{
"actionName": action,
}).Infof("StartAction")
actualAction, err := findAction(action)
if err != nil {
log.Errorf("Error finding action %s, %s", err, action)
return res
}
log.Infof("Found action %s", actualAction.Title)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sh", "-c", actualAction.Shell)
stdout, stderr := cmd.Output()
res.Stdout = string(stdout)
res.Stderr = fmt.Sprintf("%s", stderr)
if ctx.Err() == context.DeadlineExceeded {
res.TimedOut = true
}
log.Infof("Command %v stdout %v", actualAction.Title, res.Stdout)
log.Infof("Command %v stderr %v", actualAction.Title, res.Stderr)
return res
}
func findAction(actionTitle string) (*config.ActionButton, error) {
for _, action := range Cfg.ActionButtons {
if action.Title == actionTitle {
return &action, nil
}
}
return nil, errors.New("Action not found")
}

View File

@ -3,12 +3,19 @@ package grpcApi;
import ( import (
"google.golang.org/grpc" "google.golang.org/grpc"
pb "github.com/jamesread/OliveTin/gen/grpc" pb "github.com/jamesread/OliveTin/gen/grpc"
"fmt"
"net" "net"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
ctx "context" ctx "context"
config "github.com/jamesread/OliveTin/pkg/config"
executor "github.com/jamesread/OliveTin/pkg/executor"
) )
var (
cfg *config.Config;
)
type OliveTinApi struct { type OliveTinApi struct {
} }
@ -16,9 +23,7 @@ type OliveTinApi struct {
func (api *OliveTinApi) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) { func (api *OliveTinApi) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
res := &pb.StartActionResponse{} res := &pb.StartActionResponse{}
log.WithFields(log.Fields{ executor.ExecAction(req.ActionName)
"actionName": req.ActionName,
}).Infof("StartAction")
return res, nil return res, nil
} }
@ -26,25 +31,23 @@ func (api *OliveTinApi) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
func (api *OliveTinApi) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (*pb.GetButtonsResponse, error) { func (api *OliveTinApi) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (*pb.GetButtonsResponse, error) {
res := &pb.GetButtonsResponse{}; res := &pb.GetButtonsResponse{};
btn1 := pb.ActionButton { for _, action := range cfg.ActionButtons {
Title: "foo", btn := pb.ActionButton {
Icon: "&#x1F1E6", Title: action.Title,
}; Icon: lookupHtmlIcon(action.Icon),
}
btn2 := pb.ActionButton { res.Actions = append(res.Actions, &btn);
Title: "bar", }
Icon: "&#x1F1E6",
};
res.Actions = append(res.Actions, &btn1);
res.Actions = append(res.Actions, &btn2);
return res, nil return res, nil
} }
func Start() { func Start(listenAddress string, globalConfig *config.Config) {
port := 1337; cfg = globalConfig
lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port));
lis, err := net.Listen("tcp", listenAddress);
if err != nil { if err != nil {
log.Fatalf("Failed to listen - %v", err); log.Fatalf("Failed to listen - %v", err);

14
pkg/grpcApi/utils.go Normal file
View File

@ -0,0 +1,14 @@
package grpcApi
var emojis = map[string]string {
"poop": "💩",
"smile": "😀",
}
func lookupHtmlIcon(keyToLookup string) (string) {
if emoji, found := emojis[keyToLookup]; found {
return emoji;
}
return keyToLookup;
}

View File

@ -2,7 +2,6 @@ package restApi;
import ( import (
"google.golang.org/grpc" "google.golang.org/grpc"
"fmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"context" "context"
"github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/grpc-ecosystem/grpc-gateway/runtime"
@ -10,28 +9,32 @@ import (
"strings" "strings"
gw "github.com/jamesread/OliveTin/gen/grpc" gw "github.com/jamesread/OliveTin/gen/grpc"
config "github.com/jamesread/OliveTin/pkg/config"
)
var (
cfg *config.Config;
) )
func Start() (error) { func Start(listenAddressRest string, listenAddressGrpc string, globalConfig *config.Config) (error) {
port := 1339; cfg = globalConfig
ctx := context.Background() ctx := context.Background()
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
//lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port));
mux := runtime.NewServeMux() mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()} opts := []grpc.DialOption{grpc.WithInsecure()}
err := gw.RegisterOliveTinApiHandlerFromEndpoint(ctx, mux, "127.0.0.1:1337", opts) err := gw.RegisterOliveTinApiHandlerFromEndpoint(ctx, mux, listenAddressGrpc, opts)
if err != nil { if err != nil {
log.Fatalf("gw error %v", err) log.Fatalf("gw error %v", err)
} }
return http.ListenAndServe(fmt.Sprintf(":%d", port), allowCors(mux)) return http.ListenAndServe(listenAddressRest, allowCors(mux))
} }
func allowCors(h http.Handler) http.Handler { func allowCors(h http.Handler) http.Handler {

View File

@ -0,0 +1,27 @@
package staticWebserverForUi
import (
"net/http"
"encoding/json"
log "github.com/sirupsen/logrus"
)
type WebUiSettings struct {
Rest string
}
func Start(listenAddress string, listenAddressRest string) {
http.Handle("/", http.FileServer(http.Dir("webui")))
http.HandleFunc("/webUiSettings.json", func(w http.ResponseWriter, r *http.Request) {
ret := WebUiSettings {
Rest: "http://" + listenAddressRest + "/",
}
jsonRet, _ := json.Marshal(ret)
w.Write([]byte(jsonRet));
})
log.Fatal(http.ListenAndServe(listenAddress, nil))
}

View File

@ -1,9 +0,0 @@
'use strict'
import { loadContents } from './js/loader.js'
window.fetch('http://mindstorm4:1339/GetButtons').then(res => {
return res.json()
}).then(res => {
loadContents(res)
})

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -5,7 +5,7 @@ class ActionButton extends window.HTMLButtonElement {
this.stateLabels = [] this.stateLabels = []
this.currentState = 0 this.currentState = 0
this.isWaiting = false this.isWaiting = false
this.actionCallUrl = 'http://mindstorm4:1339/StartAction?actionName=' + this.title this.actionCallUrl = window.restBaseUrl + 'StartAction?actionName=' + this.title
if (json.icon !== undefined) { if (json.icon !== undefined) {
this.unicodeIcon = unescape(json.icon) this.unicodeIcon = unescape(json.icon)

38
webui/main.js Normal file
View File

@ -0,0 +1,38 @@
'use strict'
import { loadContents } from './js/loader.js'
/**
* Design choice; define this as a "global function" (on window) so that it can
* easily be used anywhere without needing to import it, as it's a pretty
* fundemental thing.
*/
window.showBigError = (type, friendlyType, message) => {
console.error("Error " + type + ": ", err)
var err = document.createElement("div")
err.classList.add("error")
err.innerHTML = "<h1>Error " + friendlyType + "</h1><p>" + message + "</p><p><a href = 'http://github.com/jamesread/OliveTin' target = 'blank'/>OliveTin Documentation</a></p>";
document.getElementById('rootGroup').appendChild(err)
}
function onInitialLoad(res) {
window.restBaseUrl = res.Rest;
window.fetch(window.restBaseUrl + "GetButtons").then(res => {
return res.json()
}).then(res => {
loadContents(res)
}).catch(err => {
showBigError("fetch-initial-buttons", "getting initial buttons", err, "blat")
});
}
window.fetch('webUiSettings.json').then(res => {
return res.json()
}).then(res => {
onInitialLoad(res)
}).catch(err => {
showBigError("fetch-webui-settings", "getting webui settings", err);
})

View File

@ -16,6 +16,11 @@ body {
text-align: center; text-align: center;
} }
.error {
background-color: salmon;
padding: 1em;
}
div.entity { div.entity {
background-color: #222; background-color: #222;
box-shadow: 0 0 10px 0 #444; box-shadow: 0 0 10px 0 #444;