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
gen/
go.sum
OliveTin

View File

@ -3,6 +3,6 @@ default:
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/ --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

View File

@ -18,7 +18,12 @@ message StartActionRequest {
string actionName = 1;
}
message StartActionResponse {}
message StartActionResponse {
string stdout = 1;
string stderr = 2;
bool timedOut = 3;
int32 exitCode = 4;
}
service OliveTinApi {
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
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"
grpcApi "github.com/jamesread/OliveTin/pkg/grpcApi"
webuiServer "github.com/jamesread/OliveTin/pkg/webuiServer"
config "github.com/jamesread/OliveTin/pkg/config"
executor "github.com/jamesread/OliveTin/pkg/executor"
"github.com/spf13/viper"
"os"
)
@ -20,12 +22,14 @@ func init() {
log.SetLevel(log.DebugLevel) // Default to debug, to catch cfg issues
viper.AutomaticEnv();
viper.SetConfigName("config.yaml")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/olivetin/")
viper.AddConfigPath("/etc/OliveTin/")
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();
@ -42,13 +46,17 @@ func init() {
func main() {
log.WithFields(log.Fields {
"listenPortRestActions": cfg.ListenPortRestActions,
"listenPortWebUi": cfg.ListenPortWebUi,
"listenAddressGrpcActions": cfg.ListenAddressGrpcActions,
"listenAddressRestActions": cfg.ListenAddressRestActions,
"listenAddressWebUi": cfg.ListenAddressWebUi,
}).Info("OliveTin started");
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/sys v0.0.0-20210124154548-22da62e12c0c // 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
google.golang.org/genproto v0.0.0-20210224155714-063164c882e6
google.golang.org/grpc v1.37.0

View File

@ -8,6 +8,7 @@ import (
type ActionButton struct {
Title string
Icon string
Shell string
Css map[string]string `mapstructure:omitempty`
}
@ -19,8 +20,9 @@ type Entity struct {
}
type Config struct {
ListenPortRestActions int
ListenPortWebUi int
ListenAddressWebUi string
ListenAddressRestActions string
ListenAddressGrpcActions string
LogLevel string
ActionButtons []ActionButton `mapstructure:"actions"`
Entities []Entity `mapstructure:omitempty`
@ -28,8 +30,9 @@ type Config struct {
func DefaultConfig() *Config {
config := Config{};
config.ListenPortRestActions = 1337;
config.ListenPortWebUi = 1339;
config.ListenAddressWebUi = "0.0.0.0:1337"
config.ListenAddressRestActions = "0.0.0.0:1338"
config.ListenAddressGrpcActions = "0.0.0.0:1339"
config.LogLevel = "INFO"
return &config
@ -43,3 +46,4 @@ func (cfg *Config) GetLogLevel() (log.Level) {
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 (
"google.golang.org/grpc"
pb "github.com/jamesread/OliveTin/gen/grpc"
"fmt"
"net"
log "github.com/sirupsen/logrus"
ctx "context"
config "github.com/jamesread/OliveTin/pkg/config"
executor "github.com/jamesread/OliveTin/pkg/executor"
)
var (
cfg *config.Config;
)
type OliveTinApi struct {
}
@ -16,9 +23,7 @@ type OliveTinApi struct {
func (api *OliveTinApi) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
res := &pb.StartActionResponse{}
log.WithFields(log.Fields{
"actionName": req.ActionName,
}).Infof("StartAction")
executor.ExecAction(req.ActionName)
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) {
res := &pb.GetButtonsResponse{};
btn1 := pb.ActionButton {
Title: "foo",
Icon: "&#x1F1E6",
};
for _, action := range cfg.ActionButtons {
btn := pb.ActionButton {
Title: action.Title,
Icon: lookupHtmlIcon(action.Icon),
}
btn2 := pb.ActionButton {
Title: "bar",
Icon: "&#x1F1E6",
};
res.Actions = append(res.Actions, &btn);
}
res.Actions = append(res.Actions, &btn1);
res.Actions = append(res.Actions, &btn2);
return res, nil
}
func Start() {
port := 1337;
lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port));
func Start(listenAddress string, globalConfig *config.Config) {
cfg = globalConfig
lis, err := net.Listen("tcp", listenAddress);
if err != nil {
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 (
"google.golang.org/grpc"
"fmt"
log "github.com/sirupsen/logrus"
"context"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
@ -10,28 +9,32 @@ import (
"strings"
gw "github.com/jamesread/OliveTin/gen/grpc"
config "github.com/jamesread/OliveTin/pkg/config"
)
var (
cfg *config.Config;
)
func Start() (error) {
port := 1339;
func Start(listenAddressRest string, listenAddressGrpc string, globalConfig *config.Config) (error) {
cfg = globalConfig
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
//lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port));
mux := runtime.NewServeMux()
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 {
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 {

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.currentState = 0
this.isWaiting = false
this.actionCallUrl = 'http://mindstorm4:1339/StartAction?actionName=' + this.title
this.actionCallUrl = window.restBaseUrl + 'StartAction?actionName=' + this.title
if (json.icon !== undefined) {
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;
}
.error {
background-color: salmon;
padding: 1em;
}
div.entity {
background-color: #222;
box-shadow: 0 0 10px 0 #444;