Просмотр исходного кода

A blob of changes while the project is in early dev.

jamesread 5 лет назад
Родитель
Сommit
b01639b9b2

+ 16 - 0
Dockerfile

@@ -0,0 +1,16 @@
+FROM fedora
+
+USER 1001
+
+CMD mkdir -p /config /var/www/olivetin/
+
+EXPOSE 1337/tcp 
+EXPOSE 1338/tcp 
+EXPOSE 1339/tcp
+
+VOLUME /config
+
+COPY OliveTin /usr/bin/OliveTin
+COPY webui /var/www/olivetin/
+
+ENTRYPOINT [ "/usr/bin/OliveTin" ]

+ 10 - 1
Makefile

@@ -1,8 +1,17 @@
-default: 
+compile: 
 	go build -o OliveTin github.com/jamesread/OliveTin/cmd/OliveTin
 
 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 OliveTin.proto
 
+podman-image:
+	buildah bud -t olivetin
+
+podman-container:
+	podman kill olivetin
+	podman rm olivetin
+	podman create --name olivetin -p 1337:1337 -p 1338:1338 -p 1339:1339 -v /etc/OliveTin/:/config:ro olivetin
+	podman start olivetin
+
 .PHONY: grpc

+ 1 - 1
OliveTin.proto

@@ -22,7 +22,7 @@ message StartActionResponse {
 	string stdout = 1;
 	string stderr = 2;
 	bool timedOut = 3;
-	int32 exitCode = 4;
+	int64 exitCode = 4;
 }
 
 service OliveTinApi {

+ 16 - 0
README.md

@@ -12,3 +12,19 @@ listenAddressRestActions: :1337 # Listen on all addresses available, port 1337
 listenAddressWebUi: :1339
 logLevel: "INFO"
 ```
+
+## Building the container 
+
+### Podman/Docker
+
+```
+podman create --name olivetin -p 1337 -p 1338 -p 1339 -v /etc/olivetin/:/config:ro olivetin
+
+```
+
+### Buildah/Docker
+
+```
+buildah bud -t olivetin
+```
+

+ 1 - 0
cmd/OliveTin/main.go

@@ -25,6 +25,7 @@ func init() {
 	viper.SetConfigName("config.yaml")
 	viper.SetConfigType("yaml")
 	viper.AddConfigPath(".")
+	viper.AddConfigPath("/config") // For containers.
 	viper.AddConfigPath("/etc/OliveTin/")
 
 	if err := viper.ReadInConfig(); err != nil {

+ 1 - 0
pkg/config/config.go

@@ -10,6 +10,7 @@ type ActionButton struct {
 	Icon string
 	Shell string
 	Css map[string]string `mapstructure:omitempty`
+	Timeout int
 }
 
 type Entity struct {

+ 33 - 0
pkg/cors/cors.go

@@ -0,0 +1,33 @@
+package cors
+
+import (
+	"net/http"
+	"strings"
+	log "github.com/sirupsen/logrus"
+)
+
+func AllowCors(h http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if origin := r.Header.Get("Origin"); origin != "" {
+			log.Infof("Setting CORS header: %v", origin)
+
+			w.Header().Set("Access-Control-Allow-Origin", origin)
+
+			if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" {
+				preflightHandler(w, r)
+				return
+			}
+		}
+
+		h.ServeHTTP(w, r)
+	})
+}
+
+func preflightHandler(w http.ResponseWriter, r *http.Request) {
+	log.Infof("preflight request for %s", r.URL.Path)
+
+	headers := []string{"Content-Type", "Accept"}
+	w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ","))
+	methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE"}
+	w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
+}

+ 26 - 6
pkg/executor/executor.go

@@ -9,7 +9,6 @@ import (
 	"os/exec"
 	"context"
 	"time"
-	"fmt"
 )
 
 var (
@@ -31,30 +30,51 @@ func ExecAction(action string) (*pb.StartActionResponse) {
 		return res
 	}
 
-	log.Infof("Found action %s", actualAction.Title)
+	log.WithFields(log.Fields {
+		"title": actualAction.Title,
+		"timeout": actualAction.Timeout,
+	}).Infof("Found action")
 
-	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+	ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
 	defer cancel()
 
 	cmd := exec.CommandContext(ctx, "sh", "-c", actualAction.Shell)
 	stdout, stderr := cmd.Output()
 
+	res.ExitCode = int64(cmd.ProcessState.ExitCode())
 	res.Stdout = string(stdout)
-	res.Stderr = fmt.Sprintf("%s", stderr)
+
+	if stderr == nil {
+		res.Stderr = ""
+	} else {
+		res.Stderr = stderr.Error()
+	}
 
 	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)
+	log.WithFields(log.Fields {
+		"stdout": res.Stdout,
+		"stderr": res.Stderr,
+		"timedOut": res.TimedOut,
+		"exit": res.ExitCode,
+	}).Infof("Finished command.")
 
 	return res
 }
 
+func sanitizeAction(action *config.ActionButton) {
+	if action.Timeout < 3 {
+		action.Timeout = 3
+	}
+}
+
 func findAction(actionTitle string) (*config.ActionButton, error) {
 	for _, action := range Cfg.ActionButtons {
 		if action.Title == actionTitle {
+			sanitizeAction(&action)
+
 			return &action, nil
 		}
 	}

+ 2 - 5
pkg/grpcApi/grpcApi.go

@@ -21,11 +21,7 @@ type OliveTinApi struct {
 }
 
 func (api *OliveTinApi) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
-	res := &pb.StartActionResponse{}
-
-	executor.ExecAction(req.ActionName)
-
-	return res, nil
+	return executor.ExecAction(req.ActionName), nil
 }
 
 func (api *OliveTinApi) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (*pb.GetButtonsResponse, error) {
@@ -40,6 +36,7 @@ func (api *OliveTinApi) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (
 		res.Actions = append(res.Actions, &btn);
 	}
 
+	log.Infof("getButtons: %v", res)
 
 	return res, nil
 }

+ 5 - 25
pkg/restApi/restapi.go

@@ -6,10 +6,11 @@ import (
 	"context"
 	"github.com/grpc-ecosystem/grpc-gateway/runtime"
 	"net/http"
-	"strings"
 
 	gw "github.com/jamesread/OliveTin/gen/grpc"
 
+	cors "github.com/jamesread/OliveTin/pkg/cors"
+
 	config "github.com/jamesread/OliveTin/pkg/config"
 )
 
@@ -25,7 +26,8 @@ func Start(listenAddressRest string, listenAddressGrpc string, globalConfig *con
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
-	mux := runtime.NewServeMux()
+	// The JSONPb.EmitDefaults is necssary, so "empty" fields are returned in JSON.
+	mux := runtime.NewServeMux(runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{OrigName: true, EmitDefaults: true}))
 	opts := []grpc.DialOption{grpc.WithInsecure()}
 
 	err := gw.RegisterOliveTinApiHandlerFromEndpoint(ctx, mux, listenAddressGrpc, opts)
@@ -34,27 +36,5 @@ func Start(listenAddressRest string, listenAddressGrpc string, globalConfig *con
 		log.Fatalf("gw error %v", err)
 	}
 
-	return http.ListenAndServe(listenAddressRest, allowCors(mux))
-}
-
-func allowCors(h http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		if origin := r.Header.Get("Origin"); origin != "" {
-			w.Header().Set("Access-Control-Allow-Origin", origin)
-			if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" {
-				preflightHandler(w, r)
-				return
-			}
-		}
-		h.ServeHTTP(w, r)
-	})
-}
-
-func preflightHandler(w http.ResponseWriter, r *http.Request) {
-	headers := []string{"Content-Type", "Accept"}
-	w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ","))
-	methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE"}
-	w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
-	log.Infof("preflight request for %s", r.URL.Path)
-	return
+	return http.ListenAndServe(listenAddressRest, cors.AllowCors(mux))
 }

+ 23 - 2
pkg/webuiServer/webuiServer.go

@@ -4,14 +4,35 @@ import (
 	"net/http"
 	"encoding/json"
 	log "github.com/sirupsen/logrus"
+	cors "github.com/jamesread/OliveTin/pkg/cors"
+	"os"
 )
 
 type WebUiSettings struct {
 	Rest string
 }
 
+func findWebuiDir() string {
+	directoriesToSearch := []string{
+		"./webui",
+		"/var/www/olivetin/",
+	}
+
+	for _, dir := range directoriesToSearch {
+		if _, err := os.Stat(dir); !os.IsNotExist(err) {
+			log.Infof("Found the webui directory here: %v", dir)
+
+			return dir
+		}
+	}
+
+	log.Warnf("Did not find the webui directory, you will probably get 404 errors.")
+
+	return "./webui" // Should not exist
+}
+
 func Start(listenAddress string, listenAddressRest string) {
-	http.Handle("/", http.FileServer(http.Dir("webui")))
+	http.Handle("/", cors.AllowCors(http.FileServer(http.Dir(findWebuiDir()))))
 
 	http.HandleFunc("/webUiSettings.json", func(w http.ResponseWriter, r *http.Request) {
 		ret := WebUiSettings {
@@ -23,5 +44,5 @@ func Start(listenAddress string, listenAddressRest string) {
 		w.Write([]byte(jsonRet));
 	})
 
-	log.Fatal(http.ListenAndServe(listenAddress, nil))
+	log.Fatal(http.ListenAndServe(listenAddress, nil));
 }

+ 12 - 6
webui/js/classes.js → webui/js/ActionButton.js

@@ -7,10 +7,10 @@ class ActionButton extends window.HTMLButtonElement {
     this.isWaiting = false
     this.actionCallUrl = window.restBaseUrl + 'StartAction?actionName=' + this.title
 
-    if (json.icon !== undefined) {
-      this.unicodeIcon = unescape(json.icon)
-    } else {
+    if (json.icon == "") {
       this.unicodeIcon = '&#x1f4a9'
+    } else {
+      this.unicodeIcon = unescape(json.icon)
     }
 
     this.onclick = () => { this.startAction() }
@@ -30,17 +30,23 @@ class ActionButton extends window.HTMLButtonElement {
         return res.json()
       }
     }).then(json => {
-      this.onActionResult()
+      if (json.timedOut) {
+        this.onActionResult('actionTimedOut')
+      } else if (json.exitCode != 0) {
+        this.onActionResult('actionNonZeroExit')
+      } else {
+        this.onActionResult('actionSuccess')
+      }
     }).catch(err => {
       this.onActionError(err)
     })
   }
 
-  onActionResult (json) {
+  onActionResult (cssClass) {
     this.disabled = false
     this.isWaiting = false
     this.updateHtml()
-    this.classList.add('actionSuccess')
+    this.classList.add(cssClass)
   }
 
   onActionError (err) {

+ 2 - 2
webui/js/loader.js → webui/js/marshaller.js

@@ -1,6 +1,6 @@
-import './classes.js' // To define action-button
+import './ActionButton.js' // To define action-button
 
-export function loadContents (json) {
+export function marshalActionButtonsJsonToHtml(json) {
   for (const jsonButton of json.actions) {
     const a = document.createElement('button', { is: 'action-button' })
     a.constructFromJson(jsonButton)

+ 9 - 6
webui/main.js

@@ -1,6 +1,6 @@
 'use strict'
 
-import { loadContents } from './js/loader.js'
+import { marshalActionButtonsJsonToHtml } from './js/marshaller.js'
 
 /**
  * Design choice; define this as a "global function" (on window) so that it can 
@@ -14,18 +14,21 @@ window.showBigError = (type, friendlyType, message) => {
 	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)
+  document.getElementById('rootGroup').appendChild(err)
 }
 
 function onInitialLoad(res) {
   window.restBaseUrl = res.Rest;
 
-  window.fetch(window.restBaseUrl + "GetButtons").then(res => {
-	return res.json()
+  window.fetch(window.restBaseUrl + "GetButtons", {
+    cors: 'cors',
+    // No fetch options
   }).then(res => {
-	loadContents(res)
+    return res.json()
+  }).then(res => {
+    marshalActionButtonsJsonToHtml(res)
   }).catch(err => {
-	showBigError("fetch-initial-buttons", "getting initial buttons", err, "blat")
+    showBigError("fetch-initial-buttons", "getting initial buttons", err, "blat")
   });
 }
 

+ 51 - 7
webui/style.css

@@ -1,6 +1,6 @@
 body {
-	background-color: #333;
-	color: white;
+	background-color: #efefef;
+	color: black;
 	text-align: center;
 	font-family: sans-serif;
 	padding: 0;
@@ -9,7 +9,7 @@ body {
 
 .group {
 	display: grid;
-	grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+	grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
 	grid-template-rows: auto auto auto auto;
 	padding: 1em;
 	grid-gap: 1em;
@@ -37,12 +37,12 @@ div.entity h2 {
 
 button {
 	padding: 1em;
-	color: white;
+	color: black;
 	display: table-cell;
 	text-align: center;
-	border: 1px solid #666;
-	background-color: #222;
-	box-shadow: 0 0 10px 0 #444;
+	border: 1px solid #999;
+	background-color: #dee3e7;
+	box-shadow: 0 0 6px 0 #aaa;
 	user-select: none;
 }
 
@@ -85,6 +85,27 @@ span.icon {
 	0% { background-color: inherit; }
 }
 
+.actionNonZeroExit {
+	animation: kfActionNonZeroExit 1s;
+}
+
+@keyframes kfActionNonZeroExit {
+	0% { background-color: black; }
+	20% { background-color: blue; }
+	0% { background-color: inherit; }
+}
+
+.actionTimeout {
+	animation: kfActionTimeout 1s;
+}
+
+@keyframes kfActionTimeout {
+	0% { background-color: black; }
+	20% { background-color: cyan; }
+	0% { background-color: inherit; }
+}
+
+
 footer, footer a {
 	color: gray;
 }
@@ -94,3 +115,26 @@ img.logo {
 	height: 1em;
 	vertical-align: middle;
 }
+
+@media (max-width: 320px) {
+	.group {
+		grid-template-columns: auto;
+		background-color: red;
+		grid-gap: 0;
+		padding: 0;
+	}
+}
+
+@media (prefers-color-scheme: dark) {
+	body {
+		background-color: #333;
+		color: white;
+	}
+
+	button {
+		border: 1px solid #666;
+		background-color: #222;
+		box-shadow: 0 0 6px 0 #444;
+		color: white;
+	}
+}