Compare commits

...

69 Commits

Author SHA1 Message Date
jamesread
59f214fd45 cicd: Add target integration testing image 2022-01-06 00:09:25 +00:00
jamesread
acaf28e200 cicd: Fix broken unit test for hidden nav 2022-01-06 00:00:54 +00:00
James Read
c263a84aa7 Merge pull request #32 from OliveTin/exec-without-shell
bugfix: Working stderr!
2022-01-05 10:53:40 +00:00
jamesread
f09501278a fmt: ActionButton - single quote strings 2022-01-05 09:59:45 +00:00
jamesread
5edeace62e bugfix: Working stderr! 2022-01-05 09:54:40 +00:00
James Read
07ca9f21bc Add CII Best Practices badge
A reminder to focus on project quality.
2022-01-03 00:28:31 +00:00
jamesread
4c7b4ee7de cicd: Build armv6 package, remove docker from arm builds to speed things up. 2021-11-26 19:39:54 +00:00
jamesread
908edc352f cicd: Remove Jenkins 2021-11-23 23:23:49 +00:00
jamesread
fbea7ba928 cicd: Remove APK support - it isnt tested, and probably does not work. It slows down the build and uses disk space. 2021-11-23 15:38:01 +00:00
jamesread
edb0ebbda4 cicd: rename arm builds to arm32 2021-11-23 15:30:27 +00:00
jamesread
d8fa35087e cicd: Build v5 and v7 2021-11-23 14:53:08 +00:00
jamesread
299f492675 bugfix: Regression, goarm was reset to 7, it should be 5 2021-11-22 23:08:20 +00:00
jamesread
f4ff0a209d cicd: Accidently didnt comment out swagger 2021-11-22 11:14:05 +00:00
jamesread
f91e5a7751 cicd: Comment out swagger, as not going to update it very often 2021-11-21 01:22:19 +00:00
jamesread
2b9e763e02 feature: Apple touch icons (#24), accessibility 2021-11-21 01:15:48 +00:00
jamesread
7bdb99764c doc: openapi/swagger Generation for docs 2021-11-21 01:01:06 +00:00
James Read
7d0b73c169 doc: Better short config.yaml in README 2021-11-20 01:07:54 +00:00
James Read
8d3a2ad223 docs: Updated 1-line description + use cases
Tried to more clearly describe the value of OliveTin - safe and simple access to shell commands. It's also not just a Linux tool - so removed the reference to Linux. Simplied the use cases with clearer, non-personal examples based on what members of the community have been doing.
2021-11-20 00:21:59 +00:00
jamesread
a1501ebbe3 doc: Add useful links to release notes 2021-11-19 17:31:03 +00:00
jamesread
9ca94756e5 cicd: Require commit messages are tagged 2021-11-19 15:09:02 +00:00
jamesread
54d6855b3d qemu setup for tag 2021-11-19 12:33:50 +00:00
jamesread
f3231655fa Oh for the love of YAML. 2021-11-19 11:56:05 +00:00
jamesread
aef1e4db1b try qemu static from GH action 2021-11-19 11:54:31 +00:00
jamesread
d5c008188e qemu-user-static 2021-11-19 00:16:37 +00:00
jamesread
d139f24d13 Try only running code checks when code changes. 2021-11-18 12:56:18 +00:00
jamesread
5b4f51f698 Possible solution for multiarch container images 2021-11-18 12:20:08 +00:00
jamesread
af5889a04d Refining the docker images 2021-11-17 20:42:57 +00:00
jamesread
e1ccf444ce platform tag on docker images 2021-11-17 18:46:15 +00:00
jamesread
9943d1ced5 Updated name of token 2021-11-17 15:22:48 +00:00
jamesread
31411d0e95 The release builder 2021-11-17 13:43:27 +00:00
jamesread
8e3112ee16 Vagrant testing support 2021-11-17 13:29:10 +00:00
jamesread
395c5bea99 Still set currentVersion if update checking is disabled 2021-11-17 11:48:24 +00:00
jamesread
7ee404f44c Restored goreleaser packages for now, as I just want to release. Still want them separate later. 2021-11-17 00:38:52 +00:00
jamesread
c7bc22ac7e Rename jenkins job to match build-snapshot 2021-11-15 22:00:54 +00:00
jamesread
e3f4cd8113 Force codestyle to use go 1.16+ 2021-11-15 21:59:09 +00:00
jamesread
1b94e29721 #17 Fixing UI after change to Autonomous Custom Elements 2021-11-15 21:56:39 +00:00
jamesread
fd04922e59 Bringing up to date with head 2021-11-15 20:09:09 +00:00
jamesread
8ecaf33b1a Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2021-11-15 19:46:18 +00:00
jamesread
2771f58469 Added sensible error messages for no module support, or javascript disabled. 2021-11-15 19:45:25 +00:00
James Read
ff5d60a2dc Add snapshot badge 2021-11-09 10:45:25 +00:00
jamesread
2f6a975bb3 Update workflows 2021-11-09 10:43:22 +00:00
jamesread
b029c7f0ac An init script (!), and ignore bins in root 2021-11-08 11:09:50 +00:00
jamesread
9b2b866701 Only archive the... archives (not the individual bins) 2021-11-04 11:51:37 +00:00
jamesread
d2c25a35f0 make grpc 2021-11-04 11:38:47 +00:00
jamesread
16b43b7b4f goreleaser action 2021-11-04 11:35:07 +00:00
jamesread
e37a653655 rc build action fix syntax 2021-11-04 10:58:11 +00:00
jamesread
a23d5265b8 Try to use a more recent version of go 2021-11-04 10:56:39 +00:00
jamesread
850fe8d704 typo here too :/ 2021-11-04 10:52:40 +00:00
jamesread
5be6934ca6 typo 2021-11-04 10:51:48 +00:00
jamesread
4e8f20e1e6 dont need to run make grpc on every build 2021-11-04 10:51:00 +00:00
jamesread
f9526749eb Add github rc build 2021-11-04 10:50:47 +00:00
jamesread
2e45f9304f Added awesome badge 2021-11-04 10:40:54 +00:00
jamesread
41dc1d9b72 remove errcheck 2021-11-04 10:34:25 +00:00
jamesread
acde5f1fd5 gocritic dep in makefile 2021-11-04 10:33:47 +00:00
jamesread
b3b5b6fe60 codestyle stuff 2021-11-04 10:32:53 +00:00
jamesread
91ce4e93a2 codestyle, fmt, unit tests, etc 2021-11-04 09:35:51 +00:00
jamesread
08eff24dda Spelling mistake 2021-11-04 08:45:15 +00:00
jamesread
78efc5c94e Yay more unit tests 2021-11-04 00:30:36 +00:00
jamesread
3aa7c97bfb Search additional dirs for the webui dir 2021-11-03 22:52:38 +00:00
jamesread
12475cd310 gofmt emoji.go 2021-11-03 22:17:37 +00:00
jamesread
80f3b29d2b formatted tools.go 2021-11-03 22:16:56 +00:00
jamesread
6357c9dc61 Add default title to choices 2021-11-02 22:01:01 +00:00
jamesread
4b2ef44959 remove old target 2021-11-02 19:34:32 +00:00
jamesread
b97fa9ed4a configs path 2021-11-02 17:14:18 +00:00
jamesread
bc73ba340c emojis are not runes 2021-11-02 16:30:54 +00:00
jamesread
c4b6c39dc9 Updated path to config 2021-11-02 16:30:00 +00:00
jamesread
08f32627fc Consolidated configs 2021-11-02 16:27:53 +00:00
jamesread
666d29cd03 Consolidated configs 2021-11-02 16:27:47 +00:00
jamesread
2a767199e2 Improved default config to include TA examples 2021-11-02 16:04:34 +00:00
58 changed files with 1042 additions and 449 deletions

28
.githooks/commit-msg Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env python3
import sys
commitmsg = ""
with open('.git/COMMIT_EDITMSG', mode='r') as f:
commitmsg = f.readline().strip()
print("Commit message is: " + commitmsg)
ALLOWED_COMMIT_TYPES = [
"cicd",
"typo",
"fmt",
"doc",
"bugfix",
"security",
"feature",
]
for allowedType in ALLOWED_COMMIT_TYPES:
if commitmsg.startswith(allowedType + ":"):
print("Allowing commit type: ", allowedType)
sys.exit(0)
print("Commit message should start with commit type. One of: ", ", ".join(ALLOWED_COMMIT_TYPES))
sys.exit(1)

44
.github/workflows/build-snapshot.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: "Build Snapshot"
on: [push]
jobs:
build-snapshot:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
image: tonistiigi/binfmt:latest
platforms: arm64,arm
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '^1.16.0'
- name: grpc
run: make grpc
- name: goreleaser
uses: goreleaser/goreleaser-action@v2
with:
distribution: goreleaser
version: latest
args: release --snapshot --rm-dist
- name: Archive binaries
uses: actions/upload-artifact@v2
with:
name: dist
path: dist/OliveTin*.*
- name: Archive integration tests
uses: actions/upload-artifact@v2
with:
name: integration-tests
path: integration-tests

55
.github/workflows/build-tag.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: "Build Tag"
on:
push:
tags:
- '*'
jobs:
build-tag:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
image: tonistiigi/binfmt:latest
platforms: arm64,arm
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '^1.16.0'
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_KEY }}
- name: grpc
run: make grpc
- name: goreleaser
uses: goreleaser/goreleaser-action@v2
with:
distribution: goreleaser
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Archive binaries
uses: actions/upload-artifact@v2
with:
name: dist
path: dist/OliveTin*.*
- name: Archive integration tests
uses: actions/upload-artifact@v2
with:
name: integration-tests
path: integration-tests

View File

@@ -13,6 +13,12 @@ name: "CodeQL"
on:
push:
paths:
- 'cmd/**'
- 'internal/**'
- 'webui/**'
- 'integration-tests/**'
- 'OliveTin.proto'
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above

View File

@@ -1,6 +1,14 @@
name: "Codestyle checks"
on: [push]
on:
push:
paths:
- 'cmd/**'
- 'internal/**'
- 'webui/**'
- 'integration-tests/**'
- 'OliveTin.proto'
jobs:
codestyle:
@@ -11,6 +19,11 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '^1.16.0'
- name: deps
run: make grpc
- name: daemon
run: make daemon-codestyle

View File

@@ -1,23 +0,0 @@
name: "Jenkins RC Build"
on:
push:
branches: [ main ]
workflow_dispatch:
inputs:
reason:
description: "Reason"
required: true
default: "no reason given"
jobs:
jenkins-trigger:
runs-on: ubuntu-latest
steps:
- name: Trigger jenkins job
uses: appleboy/jenkins-action@master
with:
url: ${{ secrets.JENKINS_URL }}
user: ${{ secrets.JENKINS_USER }}
token: ${{ secrets.JENKINS_TOKEN }}
job: "OliveTin/OliveTin-rc-builder"

6
.gitignore vendored
View File

@@ -3,9 +3,9 @@ webui/node_modules
**/*.swp
**/*.swo
gen/
OliveTin
OliveTin.armhf
OliveTin.exe
/OliveTin
/OliveTin.armhf
/OliveTin.exe
reports
releases/
dist/

View File

@@ -1,8 +1,8 @@
project_name: OliveTin
before:
hooks:
- go mod tidy
- make grpc
- go mod download
- rm -rf webui/node_modules
builds:
- env:
- CGO_ENABLED=0
@@ -18,6 +18,8 @@ builds:
goarm:
- 5 # For old RPIs
- 6
- 7
main: cmd/OliveTin/main.go
@@ -50,7 +52,7 @@ archives:
format: tar.gz
files:
- configs/config.yaml
- config.yaml
- LICENSE
- README.md
- Dockerfile
@@ -59,11 +61,105 @@ archives:
replacements:
darwin: macOS
arm: arm32v
name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}"
name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
wrap_in_directory: true
format_overrides:
- goos: windows
format: zip
dockers:
- image_templates:
- "docker.io/jamesread/olivetin:{{ .Tag }}-amd64"
dockerfile: Dockerfile
goos: linux
goarch: amd64
skip_push: false
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Tag}}"
- "--platform=linux/amd64"
extra_files:
- webui
- image_templates:
- "docker.io/jamesread/olivetin:{{ .Tag }}-arm64"
dockerfile: Dockerfile.arm64
goos: linux
goarch: arm64
skip_push: false
build_flag_templates:
- "--platform=linux/arm64"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Tag}}"
extra_files:
- webui
# This container image actually uses the goarm v5 binary, because v5 helps
# support rpi 1's, but it seems most container images start with v7.
- image_templates:
- "docker.io/jamesread/olivetin:{{ .Tag }}-armv7"
dockerfile: Dockerfile.armv7
goos: linux
goarch: arm
goarm: 7
skip_push: false
build_flag_templates:
- "--platform=linux/arm/v7"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Tag}}"
extra_files:
- webui
docker_manifests:
- name_template: docker.io/jamesread/olivetin:{{ .Version }}
image_templates:
- docker.io/jamesread/olivetin:{{ .Version }}-amd64
- docker.io/jamesread/olivetin:{{ .Version }}-arm64
- docker.io/jamesread/olivetin:{{ .Version }}-armv7
- name_template: docker.io/jamesread/olivetin:latest
image_templates:
- docker.io/jamesread/olivetin:{{ .Version }}-amd64
- docker.io/jamesread/olivetin:{{ .Version }}-arm64
- docker.io/jamesread/olivetin:{{ .Version }}-armv7
nfpms:
- maintainer: James Read <contact@jread.com>
description: OliveTin is a web interface for running Linux shell commands.
homepage: https://github.com/jamesread/OliveTin
license: AGPL-3.0
formats:
- deb
- rpm
contents:
- src: OliveTin.service
dst: /etc/systemd/system/OliveTin.service
- src: webui
dst: /var/www/olivetin/
- src: config.yaml
dst: /etc/OliveTin/config.yaml
type: "config|noreplace"
release:
footer: |
## Useful links
- [Which download do I need?](https://docs.olivetin.app/choose-package.html)
- [Ask for help and chat with others users in the Discord community](https://discord.gg/jhYWWpNJ3v)
Thanks for your interest in OliveTin!

View File

@@ -1,14 +1,15 @@
FROM fedora
FROM --platform=linux/amd64 docker.io/amd64/fedora
RUN useradd -rm olivetin -u 1000
RUN mkdir -p /config /var/www/olivetin/ && \
RUN mkdir -p /config /var/www/olivetin \
&& \
dnf install -y \
iputils \
openssh-clients \
docker \
&& dnf clean all && \
rm -rf /var/cache/yum # install ping
rm -rf /var/cache/dnf
EXPOSE 1337/tcp

22
Dockerfile.arm64 Normal file
View File

@@ -0,0 +1,22 @@
FROM --platform=linux/arm64 docker.io/arm64v8/fedora
RUN useradd -rm olivetin -u 1000
RUN mkdir -p /config /var/www/olivetin \
&& \
dnf install -y \
iputils \
openssh-clients \
&& dnf clean all && \
rm -rf /var/cache/dnf
EXPOSE 1337/tcp
VOLUME /config
COPY OliveTin /usr/bin/OliveTin
COPY webui /var/www/olivetin/
USER olivetin
ENTRYPOINT [ "/usr/bin/OliveTin" ]

22
Dockerfile.armv7 Normal file
View File

@@ -0,0 +1,22 @@
FROM --platform=linux/arm/v7 arm32v7/fedora:latest
RUN useradd -rm olivetin -u 1000
RUN mkdir -p /config /var/www/olivetin \
&& \
dnf install -y \
iputils \
openssh-clients \
&& dnf clean all && \
rm -rf /var/cache/dnf
EXPOSE 1337/tcp
VOLUME /config
COPY OliveTin /usr/bin/OliveTin
COPY webui /var/www/olivetin/
USER olivetin
ENTRYPOINT [ "/usr/bin/OliveTin" ]

View File

@@ -15,21 +15,26 @@ daemon-codestyle:
go fmt ./...
go vet ./...
gocyclo -over 4 cmd internal
gocritic check ./...
daemon-unittests:
mkdir -p reports
go test ./... -coverprofile reports/unittests.out
go tool cover -html=reports/unittests.out -o reports/unittests.html
githooks:
cp -v .githooks/* .git/hooks/
go-tools:
go install "github.com/bufbuild/buf/cmd/buf"
go install "github.com/fzipp/gocyclo/cmd/gocyclo"
go install "github.com/go-critic/go-critic/cmd/gocritic"
go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
go install "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
go install "google.golang.org/protobuf/cmd/protoc-gen-go"
go install "github.com/fzipp/gocyclo/cmd/gocyclo"
grpc: go-tools
grpc: githooks go-tools
buf generate
podman-image:
@@ -41,6 +46,11 @@ podman-container:
podman create --name olivetin -p 1337:1337 -v /etc/OliveTin/:/config:ro olivetin
podman start olivetin
integration-tests-docker-image:
docker rm -f olivetin && docker rmi -f olivetin
docker build -t olivetin:latest .
docker create --name olivetin -p 1337:1337 -v `pwd`/integration-tests/configs/:/config/ olivetin
devrun: compile
killall OliveTin || true
./OliveTin &

View File

@@ -2,21 +2,32 @@
<img alt = "project logo" src = "https://github.com/OliveTin/OliveTin/blob/main/webui/OliveTinLogo.png" align = "right" width = "160px" />
OliveTin is a web interface for running Linux shell commands.
OliveTin gives **safe** and **simple** access to predefined shell commands from a web interface.
[![Discord](https://img.shields.io/discord/846737624960860180?label=Discord%20Server)](https://discord.gg/jhYWWpNJ3v)
[![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/awesome-selfhosted/awesome-selfhosted#automation)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5050/badge)](https://bestpractices.coreinfrastructure.org/projects/5050)
[![Go Report Card](https://goreportcard.com/badge/github.com/Olivetin/OliveTin)](https://goreportcard.com/report/github.com/OliveTin/OliveTin)
[![Build Snapshot](https://github.com/OliveTin/OliveTin/actions/workflows/build-snapshot.yml/badge.svg)](https://github.com/OliveTin/OliveTin/actions/workflows/build-snapshot.yml)
## Use cases
Some example **use cases**;
**Safely** give access to commands, for less technical people;
1. Give controlled access to run shell commands to less technical folks who cannot be trusted with SSH. I use this so my family can `podman restart plex` without asking me, and without giving them shell access!
2. Great for home automation tablets stuck on walls around your house - I use this to turn Hue lights on and off for example.
3. Sometimes SSH access isn't possible to a server, or you are feeling too lazy to type a long command you run regularly! I use this to send Wake on Lan commands to servers around my house.
* eg: Give your family a button to `podman restart plex`
* eg: Give junior admins a simple web form with dropdowns, to start your custom script. `backupScript.sh --folder {{ customerName }}`
* eg: Enable SSH access to the server for the next 20 mins `firewall-cmd --add-service ssh --timeout 20m`
[Join the community on Discord.](https://discord.gg/jhYWWpNJ3v)
**Simplify** complex commands, make them accessible and repeatable;
## YouTube video demo (6 mins)
* eg: Expose complex commands on touchscreen tablets stuck on walls around your house. `wake-on-lan aa:bb:cc:11:22:33`
* eg: Run long running on your servers from your cell phone. `dnf update -y`
* eg: Define complex commands with lots of preset arguments, and turn a few arguments into dropdown select boxes. `docker rm {{ container }} && docker create {{ container }} && docker start {{ container }}`
[Join the community on Discord](https://discord.gg/jhYWWpNJ3v) to talk with other users about use cases, or to ask for support in getting started.
## YouTube demo video (6 mins)
[![6 minute demo video](https://img.youtube.com/vi/Ej6NM9rmZtk/0.jpg)](https://www.youtube.com/watch?v=Ej6NM9rmZtk)
@@ -66,20 +77,31 @@ logLevel: "INFO"
actions:
# Docs: https://docs.olivetin.app/action-container-control.html
- title: Restart Plex
icon: smile
icon: restart
shell: docker restart plex
# This will send 1 ping
# Docs: https://docs.olivetin.app/action-ping.html
- title: Ping Google.com
shell: ping google.com -c 1
- title: Ping host
shell: ping {{ host }} -c {{ count }}
icon: ping
arguments:
- name: host
title: host
type: ascii_identifier
default: example.com
- name: count
title: Count
type: int
default: 1
# Restart lightdm on host "overseer"
# Restart http on host "webserver1"
# Docs: https://docs.olivetin.app/action-ssh.html
- title: restart lightdm
icon: poop
shell: ssh root@overseer 'service lightdm restart'
- title: restart httpd
icon: restart
shell: ssh root@webserver1 'service httpd restart'
```
A full example config can be found at in this repository - [config.yaml](https://github.com/OliveTin/OliveTin/blob/main/var/config.yaml).
A full example config can be found at in this repository - [config.yaml](https://github.com/OliveTin/OliveTin/blob/main/config.yaml).

View File

@@ -11,3 +11,10 @@ plugins:
- name: grpc-gateway
out: gen/grpc/
opt: paths=source_relative
# - name: swagger
# out: reports/swagger
# - name: openapiv2
# out: reports/openapiv2

View File

@@ -63,7 +63,7 @@ func reloadConfig() {
os.Exit(1)
}
config.Sanitize(cfg)
cfg.Sanitize()
}
func main() {

View File

@@ -10,25 +10,32 @@ logLevel: "INFO"
# Actions (buttons) to show up on the WebUI:
actions:
# This will run a simple script that you create.
- title: Run backup script
shell: /opt/backupScript.sh
icon: backup
# This will send 1 ping (-c 1)
# Docs: https://docs.olivetin.app/action-ping.html
- title: Ping Google.com
shell: ping google.com -c 1
- title: Ping host
shell: ping {{ host }} -c {{ count }}
icon: ping
arguments:
- name: host
title: host
type: ascii_identifier
default: example.com
- name: count
title: Count
type: int
default: 1
# Restart lightdm on host "overseer"
# Restart lightdm on host "server1"
# Docs: https://docs.olivetin.app/action-ping.html
- title: restart lightdm
icon: poop
shell: ssh root@overseer 'service lightdm restart'
- title: sleep 2 seconds
shell: sleep 2
icon: "&#x1F971"
- title: sleep 5 seconds (timeout)
shell: sleep 5
icon: "&#x1F62A"
- title: restart httpd
icon: restart
shell: ssh root@server1 'service httpd restart'
# OliveTin can run long-running jobs like Ansible playbooks.
#
@@ -47,7 +54,24 @@ actions:
# see the docs below.
#
# Docs: https://docs.olivetin.app/action-container-control.html
- title: Restart Plex
icon: smile
shell: docker restart plex
- title: Restart Docker Container
icon: restart
shell: docker restart {{ container }}
arguments:
- name: container
title: Container name
choices:
- value: plex
- value: traefik
- value: grafana
- title: Slow Script
shell: sleep 3
timeout: 5
icon: "&#x1F971"
- title: Broken Script (timeout)
shell: sleep 5
timeout: 5
icon: "&#x1F62A"

View File

@@ -1,53 +0,0 @@
# There is a built-in micro proxy that will host the webui and REST API all on
# one port (this is called the "Single HTTP Frontend") and means you just need
# one open port in the container/firewalls/etc.
#
# Listen on all addresses available, port 1337
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
# Choose from INFO (default), WARN and DEBUG
logLevel: "INFO"
# Actions (buttons) to show up on the WebUI:
actions:
# This will send 1 ping (-c 1)
# Docs: https://docs.olivetin.app/action-ping.html
- title: Ping Google.com
shell: ping google.com -c 1
icon: ping
# Restart lightdm on host "overseer"
# Docs: https://docs.olivetin.app/action-ping.html
- title: restart lightdm
icon: poop
shell: ssh root@overseer 'service lightdm restart'
- title: sleep 2 seconds
shell: sleep 2
icon: "&#x1F971"
- title: sleep 5 seconds (timeout)
shell: sleep 5
icon: "&#x1F62A"
# OliveTin can run long-running jobs like Ansible playbooks.
#
# For such jobs, you will need to install ansible-playbook on the host where
# you are running OliveTin, or in the container.
#
# You probably want a much longer timeout as well (so that ansible completes).
- title: "Run Ansible Playbook"
icon: "&#x1F1E6"
shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
timeout: 120
# OliveTin can control containers - docker is just a command line app.
#
# However, if you are running in a container you will need to do some setup,
# see the docs below.
#
# Docs: https://docs.olivetin.app/action-container-control.html
- title: Restart Plex
icon: smile
shell: docker restart plex

View File

@@ -1,14 +0,0 @@
# There is a built-in micro proxy that will host the webui and REST API all on
# one port (this is called the "Single HTTP Frontend") and means you just need
# one open port in the container/firewalls/etc.
#
# Listen on all addresses available, port 1337
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
hideNavigation: true
# Actions (buttons) to show up on the WebUI:
actions:
- title: Ping example.com
shell: ping example.com -c 1
icon: ping

View File

@@ -1,24 +0,0 @@
# WARNING
# This is considered an advanced installation of OliveTin, and 99% of users
# probably won't need this configuration file. If you're just getting started
# with OliveTin, don't use this.
# WARNING
#
# This tells OliveTin to not spawn the internal micro reverse proxy.
#
# This gives you more fine grained control, but requires quite a bit more setup.
# Most users will set this to "true" and use the built-in micro reverse proxy.
useSingleHTTPFrontend: false
# The WebUI is simply a static webserver. OliveTin comes with one builtin to
# make things easy, but you can also host the "webui" directory on a static
# webserver.
#listenAddressWebUI: 0.0.0.0:1340
# The REST API is used by the WebUI.
#listenAddressRestActions: 0.0.0.0:1338
# The gRPC API is unsed by the WebUI, and can also be limited to localhost:1339.
#listenAddressGrpcActions: 0.0.0.0:1339

3
go.mod
View File

@@ -8,10 +8,13 @@ require (
github.com/fsnotify/fsnotify v1.4.9
github.com/fzipp/gocyclo v0.3.1
github.com/go-co-op/gocron v1.6.2
github.com/go-critic/go-critic v0.6.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.7.0
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
golang.org/x/tools v0.1.7 // indirect
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced
google.golang.org/grpc v1.40.0-dev.0.20210708170655-30dfb4b933a5
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0

49
go.sum
View File

@@ -87,9 +87,30 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-co-op/gocron v1.6.2 h1:x5g1tWnWcXIZesdosJJcbziRi4XG6tKB92yKLUpoBkU=
github.com/go-co-op/gocron v1.6.2/go.mod h1:DbJm9kdgr1sEvWpHCA7dFFs/PGHPMil9/97EXCRPr4k=
github.com/go-critic/go-critic v0.6.1 h1:lS8B9LH/VVsvQQP7Ao5TJyQqteVKVs3E4dXiHMyubtI=
github.com/go-critic/go-critic v0.6.1/go.mod h1:SdNCfU0yF3UBjtaZGw6586/WocupMOJuiqgom5DsQxM=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astequal v1.0.1 h1:JbSszi42Jiqu36Gnf363HWS9MTEAz67vTQLponh3Moc=
github.com/go-toolsmith/astequal v1.0.1/go.mod h1:4oGA3EZXTVItV/ipGiOx7NWkY5veFfcsOJVS2YxltLw=
github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg=
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/go-toolsmith/typep v1.0.2 h1:8xdsa1+FSIH/RhEkgnD1j2CJOy5mNllW1Q9tRiYwvlk=
github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
@@ -162,6 +183,7 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@@ -216,10 +238,13 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -235,6 +260,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -247,6 +273,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
github.com/quasilyte/go-ruleguard v0.3.1-0.20210203134552-1b5a410e1cc8/go.mod h1:KsAh3x0e7Fkpgs+Q9pNLS5XpFSvYCEVl5gP9Pp1xp30=
github.com/quasilyte/go-ruleguard v0.3.13 h1:O1G41cq1jUr3cJmqp7vOUT0SokqjzmS9aESWJuIDRaY=
github.com/quasilyte/go-ruleguard v0.3.13/go.mod h1:Ul8wwdqR6kBVOCt2dipDBkE+T6vAV/iixkrKuRTN1oQ=
github.com/quasilyte/go-ruleguard/dsl v0.3.0/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/quasilyte/go-ruleguard/dsl v0.3.10/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc=
github.com/quasilyte/go-ruleguard/rules v0.0.0-20210428214800-545e0d2e0bf7/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50=
github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY=
github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@@ -291,6 +327,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
@@ -355,6 +392,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -392,6 +430,7 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -463,8 +502,10 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -482,6 +523,7 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -526,18 +568,21 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201230224404-63754364767c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -1,2 +1,3 @@
results
node_modules
.vagrant

View File

@@ -1,11 +1,3 @@
container:
rm -rf *.tar.gz
mv dist/*.tar.gz ./
tar xavf OliveTin-*linux-amd64.tar.gz
docker rm -f olivetin && docker rmi -f olivetin
docker build -t olivetin:latest OliveTin-*linux-amd64/
docker create --name olivetin -p 1337:1337 -v `pwd`/config/:/config/ olivetin
cypress:
npm install
./cypressRun.sh "general"

35
integration-tests/Vagrantfile vendored Normal file
View File

@@ -0,0 +1,35 @@
# This Vagrantfile is designed to be used with artifacts that have been built by goreleaser.
# (eg, snapshot builds on GitHub)
Vagrant.configure("2") do |config|
config.vm.box = "generic/centos8"
config.vm.provision "shell", inline: "mkdir /etc/OliveTin && chmod o+w /etc/OliveTin/", privileged: true
config.vm.provision "file", source: "configs/config.general.yaml/.", destination: "/etc/OliveTin/config.yaml"
config.vm.provider :libvirt do |libvirt|
libvirt.management_network_device = 'virbr0'
end
config.vm.define :f34 do |f34|
f34.vm.box = "generic/fedora34"
f34.vm.provision "file", source: "/opt/OliveTin-vagrant/linux_amd64_rpm/.", destination: "."
f34.vm.provision "shell", inline: "rpm -U OliveTin* && systemctl enable --now OliveTin && systemctl disable --now firewalld"
end
config.vm.define :debian do |debian|
debian.vm.box = "generic/debian10"
debian.vm.provision "file", source: "/opt/OliveTin-vagrant/linux_amd64_deb/.", destination: "."
debian.vm.provision "shell", inline: "dpkg --force-confold -i OliveTin* && systemctl enable --now OliveTin"
end
config.vm.define :ubuntu do |ubuntu|
ubuntu.vm.box = "generic/ubuntu2110"
ubuntu.vm.provision "file", source: "/opt/OliveTin-vagrant/linux_amd64_deb/.", destination: "."
ubuntu.vm.provision "shell", inline: "dpkg --force-confold -i OliveTin* && systemctl enable --now OliveTin && systemctl disable --now firewalld"
end
# TODO
#
end

View File

@@ -1,53 +0,0 @@
# There is a built-in micro proxy that will host the webui and REST API all on
# one port (this is called the "Single HTTP Frontend") and means you just need
# one open port in the container/firewalls/etc.
#
# Listen on all addresses available, port 1337
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
# Choose from INFO (default), WARN and DEBUG
logLevel: "DEBUG"
# Actions (buttons) to show up on the WebUI:
actions:
# This will send 1 ping (-c 1)
# Docs: https://docs.olivetin.app/action-ping.html
- title: Ping Google.com
shell: ping google.com -c 1
icon: ping
# Restart lightdm on host "overseer"
# Docs: https://docs.olivetin.app/action-ping.html
- title: restart lightdm
icon: poop
shell: ssh root@overseer 'service lightdm restart'
- title: sleep 2 seconds
shell: sleep 2
icon: "&#x1F971"
- title: sleep 5 seconds (timeout)
shell: sleep 5
icon: "&#x1F62A"
# OliveTin can run long-running jobs like Ansible playbooks.
#
# For such jobs, you will need to install ansible-playbook on the host where
# you are running OliveTin, or in the container.
#
# You probably want a much longer timeout as well (so that ansible completes).
- title: "Run Ansible Playbook"
icon: "&#x1F1E6"
shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
timeout: 120
# OliveTin can control containers - docker is just a command line app.
#
# However, if you are running in a container you will need to do some setup,
# see the docs below.
#
# Docs: https://docs.olivetin.app/action-container-control.html
- title: Restart Plex
icon: smile
shell: docker restart plex

View File

@@ -1,14 +0,0 @@
# There is a built-in micro proxy that will host the webui and REST API all on
# one port (this is called the "Single HTTP Frontend") and means you just need
# one open port in the container/firewalls/etc.
#
# Listen on all addresses available, port 1337
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
hideNavigation: true
# Actions (buttons) to show up on the WebUI:
actions:
- title: Ping example.com
shell: ping example.com -c 1
icon: ping

View File

@@ -1,53 +0,0 @@
# There is a built-in micro proxy that will host the webui and REST API all on
# one port (this is called the "Single HTTP Frontend") and means you just need
# one open port in the container/firewalls/etc.
#
# Listen on all addresses available, port 1337
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
# Choose from INFO (default), WARN and DEBUG
logLevel: "DEBUG"
# Actions (buttons) to show up on the WebUI:
actions:
# This will send 1 ping (-c 1)
# Docs: https://docs.olivetin.app/action-ping.html
- title: Ping Google.com
shell: ping google.com -c 1
icon: ping
# Restart lightdm on host "overseer"
# Docs: https://docs.olivetin.app/action-ping.html
- title: restart lightdm
icon: poop
shell: ssh root@overseer 'service lightdm restart'
- title: sleep 2 seconds
shell: sleep 2
icon: "&#x1F971"
- title: sleep 5 seconds (timeout)
shell: sleep 5
icon: "&#x1F62A"
# OliveTin can run long-running jobs like Ansible playbooks.
#
# For such jobs, you will need to install ansible-playbook on the host where
# you are running OliveTin, or in the container.
#
# You probably want a much longer timeout as well (so that ansible completes).
- title: "Run Ansible Playbook"
icon: "&#x1F1E6"
shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
timeout: 120
# OliveTin can control containers - docker is just a command line app.
#
# However, if you are running in a container you will need to do some setup,
# see the docs below.
#
# Docs: https://docs.olivetin.app/action-container-control.html
- title: Restart Plex
icon: smile
shell: docker restart plex

View File

@@ -0,0 +1,35 @@
#
# Integration Test Config: General
#
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
actions:
- title: Ping Google.com
shell: ping google.com -c 1
icon: ping
- title: restart lightdm
icon: poop
shell: ssh root@overseer 'service lightdm restart'
- title: sleep 2 seconds
shell: sleep 2
icon: "&#x1F971"
- title: sleep 5 seconds (timeout)
shell: sleep 5
icon: "&#x1F62A"
- title: "Run Ansible Playbook"
icon: "&#x1F1E6"
shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
timeout: 120
- title: Restart Plex
icon: smile
shell: docker restart plex

View File

@@ -0,0 +1,14 @@
#
# Integration Test Config: General
#
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
hideNavigation: true
checkForUpdates: false
# Actions (buttons) to show up on the WebUI:
actions:
- title: Ping example.com
shell: ping example.com -c 1
icon: ping

View File

@@ -12,7 +12,7 @@ describe('Homepage rendering', () => {
})
it('Switcher navigation is visible', () => {
cy.get('#switcher').then($el => {
cy.get('#sectionSwitcher').then($el => {
expect(Cypress.dom.isHidden($el)).to.be.false
})
})

View File

@@ -9,7 +9,7 @@ describe('Hidden Nav', () => {
})
it('Switcher navigation is hidden', () => {
cy.get('#switcher').then($el => {
cy.get('#sectionSwitcher').then($el => {
expect(Cypress.dom.isHidden($el)).to.be.true
})
})

View File

@@ -4,7 +4,7 @@ set -o xtrace
echo "Running config $1"
cp -f ../configs/config.$1.yaml ./config/config.yaml
cp -f ./configs/config.$1.yaml ./configs/config.yaml
docker start olivetin
NO_COLOR=1 ./node_modules/.bin/cypress run --headless -s cypress/integration/$1/* || true
docker kill olivetin

View File

@@ -0,0 +1,12 @@
#!/bin/bash
# args:
# $1: The Vagrant VM to test against. If blank and only one VM is provisioned, it will use that.
IP=$(vagrant ssh-config $1 | grep HostName | awk '{print $2}')
BASE_URL="http://$IP:1337/"
echo "IP: $IP, BaseURL: $BASE_URL"
# Only run the general test, as we cannot easily switch out configs in VMs yet.
./node_modules/.bin/cypress run --headless -c baseUrl=$BASE_URL -s cypress/integration/general/*

View File

@@ -6,10 +6,12 @@ import (
log "github.com/sirupsen/logrus"
)
// User respresents a person.
type User struct {
Username string
}
// IsAllowedExec checks if a User is allowed to execute an Action
func IsAllowedExec(cfg *config.Config, user *User, action *config.Action) bool {
canExec := cfg.DefaultPermissions.Exec
@@ -40,6 +42,7 @@ func IsAllowedExec(cfg *config.Config, user *User, action *config.Action) bool {
return canExec
}
// IsAllowedView checks if a User is allowed to view an Action
func IsAllowedView(cfg *config.Config, user *User, action *config.Action) bool {
canView := cfg.DefaultPermissions.View
@@ -75,6 +78,8 @@ func isUserInGroup(user *User, usergroup string) bool {
return true
}
// UserFromContext tries to find a user from a grpc context - obviously this is
// a stub at the moment.
func UserFromContext(ctx context.Context) *User {
return &User{
Username: "Guest",

View File

@@ -1,7 +1,7 @@
package config
import ()
// Action represents the core functionality of OliveTin - commands that show up
// as buttons in the UI.
type Action struct {
ID string
Title string
@@ -13,6 +13,7 @@ type Action struct {
Arguments []ActionArgument
}
// ActionArgument objects appear on Actions.
type ActionArgument struct {
Name string
Title string
@@ -21,6 +22,7 @@ type ActionArgument struct {
Choices []ActionArgumentChoice
}
// ActionArgumentChoice represents a predefined choice for an argument.
type ActionArgumentChoice struct {
Value string
Title string
@@ -35,17 +37,20 @@ type Entity struct {
CSS map[string]string
}
// PermissionsEntry defines what users can do with an action.
type PermissionsEntry struct {
Usergroup string
View bool
Exec bool
}
// DefaultPermissions will be used when no PermissionsEntry overrides it.
type DefaultPermissions struct {
View bool
Exec bool
}
// UserGroup is a group of users.
type UserGroup struct {
Name string
Members []string

View File

@@ -1,5 +1,6 @@
package config
// FindAction will return a action if there is a match on Title
func (cfg *Config) FindAction(actionTitle string) *Action {
for _, action := range cfg.Actions {
if action.Title == actionTitle {
@@ -9,3 +10,14 @@ func (cfg *Config) FindAction(actionTitle string) *Action {
return nil
}
// FindArg will return an arg if there is a match on Name
func (action *Action) FindArg(name string) *ActionArgument {
for _, arg := range action.Arguments {
if arg.Name == name {
return &arg
}
}
return nil
}

View File

@@ -0,0 +1,33 @@
package config
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestFindAction(t *testing.T) {
c := DefaultConfig()
a1 := Action{}
a1.Title = "a1"
c.Actions = append(c.Actions, a1)
a2 := Action{
Title: "a2",
Arguments: []ActionArgument{
{
Name: "Blat",
},
},
}
c.Actions = append(c.Actions, a2)
assert.NotNil(t, c.FindAction("a1"), "Find action a1")
assert.NotNil(t, c.FindAction("a2"), "Find action a2")
assert.NotNil(t, c.FindAction("a2").FindArg("Blat"), "Find action argument")
assert.Nil(t, c.FindAction("a2").FindArg("Blatey Cake"), "Find non-existent action argument")
assert.Nil(t, c.FindAction("waffles"), "Find non-existent action")
}

View File

@@ -1,9 +1,13 @@
package config
var emojis = map[string]string{
"poop": "&#x1f4a9;",
"smile": "&#x1F600;",
"ping": "&#x1f4e1;",
"": "&#x1F600;", // default icon
"poop": "&#x1f4a9;",
"smile": "&#x1F600;",
"ping": "&#x1f4e1;",
"backup": "&#128190;",
"reboot": "&#9211;",
"restart": "&#9211;",
}
func lookupHTMLIcon(keyToLookup string) string {

View File

@@ -4,47 +4,55 @@ import (
log "github.com/sirupsen/logrus"
)
func Sanitize(cfg *Config) {
sanitizeLogLevel(cfg)
// Sanitize will look for common configuration issues, and fix them. For example,
// populating undefined fields - name -> title, etc.
func (cfg *Config) Sanitize() {
cfg.sanitizeLogLevel()
//log.Infof("cfg %p", cfg)
// log.Infof("cfg %p", cfg)
for idx, _ := range cfg.Actions {
sanitizeAction(&cfg.Actions[idx])
for idx := range cfg.Actions {
cfg.Actions[idx].sanitize()
}
}
func sanitizeLogLevel(cfg *Config) {
func (cfg *Config) sanitizeLogLevel() {
if logLevel, err := log.ParseLevel(cfg.LogLevel); err == nil {
log.Info("Setting log level to ", logLevel)
log.SetLevel(logLevel)
}
}
func sanitizeAction(action *Action) {
func (action *Action) sanitize() {
if action.Timeout < 3 {
action.Timeout = 3
}
action.Icon = lookupHTMLIcon(action.Icon)
for idx, _ := range action.Arguments {
sanitizeActionArgument(&action.Arguments[idx])
for idx := range action.Arguments {
action.Arguments[idx].sanitize()
}
}
func sanitizeActionArgument(arg *ActionArgument) {
func (arg *ActionArgument) sanitize() {
if arg.Title == "" {
arg.Title = arg.Name
}
sanitizeActionArgumentNoType(arg)
for idx, choice := range arg.Choices {
if choice.Title == "" {
arg.Choices[idx].Title = choice.Value
}
}
arg.sanitizeNoType()
// TODO Validate the default against the type checker, but this creates a
// import loop
}
func sanitizeActionArgumentNoType(arg *ActionArgument) {
func (arg *ActionArgument) sanitizeNoType() {
if len(arg.Choices) == 0 && arg.Type == "" {
log.WithFields(log.Fields{
"arg": arg.Name,

View File

@@ -0,0 +1,38 @@
package config
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestSanitizeConfig(t *testing.T) {
c := DefaultConfig()
a := Action{
Title: "Mr Waffles",
Arguments: []ActionArgument{
{
Name: "Carrots",
Choices: []ActionArgumentChoice{
{
Value: "Waffle",
},
},
},
{
Name: "foobar",
},
},
}
c.Actions = append(c.Actions, a)
c.Sanitize()
a2 := c.FindAction("Mr Waffles")
assert.NotNil(t, a2, "Found action after adding it")
assert.Equal(t, 3, a2.Timeout, "Default timeout is set")
assert.Equal(t, "&#x1F600;", a2.Icon, "Default icon is a smiley")
assert.Equal(t, "Carrots", a2.Arguments[0].Title, "Arg title is set to name")
assert.Equal(t, "Waffle", a2.Arguments[0].Choices[0].Title, "Choice title is set to name")
}

View File

@@ -12,6 +12,7 @@ import (
"regexp"
"strings"
"time"
"bytes"
)
var (
@@ -24,6 +25,9 @@ var (
}
)
// InternalLogEntry objects are created by an Executor, and represent the final
// state of execution (even if the command is not executed). It's designed to be
// easily serializable.
type InternalLogEntry struct {
Datetime string
Stdout string
@@ -40,6 +44,8 @@ type InternalLogEntry struct {
ActionIcon string
}
// ExecutionRequest is a request to execute an action. It's passed to an
// Executor. They're created from the grpcapi.
type ExecutionRequest struct {
ActionName string
Arguments map[string]string
@@ -50,33 +56,37 @@ type ExecutionRequest struct {
finalParsedCommand string
}
type ExecutorStep interface {
type executorStep interface {
Exec(*ExecutionRequest) bool
}
// Executor represents a helper class for executing commands. It's main method
// is ExecRequest
type Executor struct {
Logs []InternalLogEntry
chainOfCommand []ExecutorStep
chainOfCommand []executorStep
}
// DefaultExecutor returns an Executor, with a sensible "chain of command" for
// executing actions.
func DefaultExecutor() *Executor {
e := Executor{}
e.chainOfCommand = []ExecutorStep{
StepFindAction{},
StepAclCheck{},
StepParseArgs{},
StepLogStart{},
StepExec{},
StepLogFinish{},
e.chainOfCommand = []executorStep{
stepFindAction{},
stepACLCheck{},
stepParseArgs{},
stepLogStart{},
stepExec{},
stepLogFinish{},
}
return &e
}
type StepFindAction struct{}
type stepFindAction struct{}
func (s StepFindAction) Exec(req *ExecutionRequest) bool {
func (s stepFindAction) Exec(req *ExecutionRequest) bool {
actualAction := req.Cfg.FindAction(req.ActionName)
if actualAction == nil {
@@ -85,7 +95,6 @@ func (s StepFindAction) Exec(req *ExecutionRequest) bool {
}).Warnf("Action not found")
req.logEntry.Stderr = "Action not found"
req.logEntry.ExitCode = -1337
return false
}
@@ -96,9 +105,9 @@ func (s StepFindAction) Exec(req *ExecutionRequest) bool {
return true
}
type StepAclCheck struct{}
type stepACLCheck struct{}
func (s StepAclCheck) Exec(req *ExecutionRequest) bool {
func (s stepACLCheck) Exec(req *ExecutionRequest) bool {
return acl.IsAllowedExec(req.Cfg, req.User, req.action)
}
@@ -107,6 +116,9 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
req.logEntry = &InternalLogEntry{
Datetime: time.Now().Format("2006-01-02 15:04:05"),
ActionTitle: req.ActionName,
Stdout: "",
Stderr: "",
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
}
for _, step := range e.chainOfCommand {
@@ -130,9 +142,9 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
}
}
type StepLogStart struct{}
type stepLogStart struct{}
func (e StepLogStart) Exec(req *ExecutionRequest) bool {
func (e stepLogStart) Exec(req *ExecutionRequest) bool {
log.WithFields(log.Fields{
"title": req.action.Title,
"timeout": req.action.Timeout,
@@ -141,9 +153,9 @@ func (e StepLogStart) Exec(req *ExecutionRequest) bool {
return true
}
type StepLogFinish struct{}
type stepLogFinish struct{}
func (e StepLogFinish) Exec(req *ExecutionRequest) bool {
func (e stepLogFinish) Exec(req *ExecutionRequest) bool {
log.WithFields(log.Fields{
"title": req.action.Title,
"stdout": req.logEntry.Stdout,
@@ -155,16 +167,14 @@ func (e StepLogFinish) Exec(req *ExecutionRequest) bool {
return true
}
type StepParseArgs struct{}
type stepParseArgs struct{}
func (e StepParseArgs) Exec(req *ExecutionRequest) bool {
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())
@@ -175,26 +185,33 @@ func (e StepParseArgs) Exec(req *ExecutionRequest) bool {
return true
}
type StepExec struct{}
type stepExec struct{}
func (e StepExec) Exec(req *ExecutionRequest) bool {
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", req.finalParsedCommand)
stdout, stderr := cmd.Output()
var stdout bytes.Buffer
var stderr bytes.Buffer
if stderr != nil {
req.logEntry.Stderr = stderr.Error()
cmd := exec.CommandContext(ctx, "sh", "-c", req.finalParsedCommand)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
runerr := cmd.Run()
req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
req.logEntry.Stdout = stdout.String()
req.logEntry.Stderr = stderr.String()
if runerr != nil {
req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
}
if ctx.Err() == context.DeadlineExceeded {
req.logEntry.TimedOut = true
}
req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
req.logEntry.Stdout = string(stdout)
return true
}
@@ -225,7 +242,7 @@ func parseActionArguments(rawShellCommand string, values map[string]string, acti
"value": argValue,
}).Debugf("Arg assigned")
rawShellCommand = strings.Replace(rawShellCommand, match[0], argValue, -1)
rawShellCommand = strings.ReplaceAll(rawShellCommand, match[0], argValue)
}
log.WithFields(log.Fields{
@@ -236,7 +253,7 @@ func parseActionArguments(rawShellCommand string, values map[string]string, acti
}
func typecheckActionArgument(name string, value string, action *config.Action) error {
arg := findArg(name, action)
arg := action.FindArg(name)
if arg == nil {
return errors.New("Action arg not defined: " + name)
@@ -256,16 +273,17 @@ func typecheckChoice(value string, arg *config.ActionArgument) error {
}
}
return errors.New("Arg value is not one of the predefined choices")
return errors.New("argument value is not one of the predefined choices")
}
// TypeSafetyCheck checks argument values match a specific type. The types are
// defined in typecheckRegex, and, you guessed it, uses regex to check for allowed
// characters.
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)
return errors.New("argument type not implemented " + typ)
}
matches, _ := regexp.MatchString(pattern, value)
@@ -277,17 +295,7 @@ func TypeSafetyCheck(name string, value string, typ string) error {
"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 errors.New("invalid argument, doesn't match " + typ)
}
return nil

View File

@@ -0,0 +1,70 @@
package executor
import (
"github.com/stretchr/testify/assert"
"testing"
acl "github.com/jamesread/OliveTin/internal/acl"
config "github.com/jamesread/OliveTin/internal/config"
)
func TestSanitizeUnsafe(t *testing.T) {
assert.Nil(t, TypeSafetyCheck("", "_zomg_ c:/ haxxor ' bobby tables && rm -rf ", "very_dangerous_raw_string"))
}
func testingExecutor() (*Executor, *config.Config) {
e := DefaultExecutor()
cfg := config.DefaultConfig()
a1 := config.Action{
Title: "Do some tickles",
Shell: "echo 'Tickling {{ person }}'",
Arguments: []config.ActionArgument{
{
Name: "person",
Type: "ascii",
},
},
}
cfg.Actions = append(cfg.Actions, a1)
cfg.Sanitize()
return e, cfg
}
func TestCreateExecutorAndExec(t *testing.T) {
e, cfg := testingExecutor()
req := ExecutionRequest{
ActionName: "Do some tickles",
User: &acl.User{Username: "Mr Tickle"},
Cfg: cfg,
Arguments: map[string]string{
"person": "yourself",
},
}
e.ExecRequest(&req)
assert.NotNil(t, e, "Create an executor")
assert.NotNil(t, e.ExecRequest(&req), "Execute a request")
assert.Equal(t, int32(0), req.logEntry.ExitCode, "Exit code is zero")
}
func TestExecNonExistant(t *testing.T) {
e, cfg := testingExecutor()
req := ExecutionRequest{
ActionName: "Waffles",
logEntry: &InternalLogEntry{},
Cfg: cfg,
}
e.ExecRequest(&req)
assert.Equal(t, int32(-1337), req.logEntry.ExitCode, "Log entry is set to an internal error code")
assert.Equal(t, "", req.logEntry.ActionIcon, "Log entry icon wasnt found")
}

View File

@@ -105,7 +105,12 @@ func Start(globalConfig *config.Config) {
grpcServer := grpc.NewServer()
pb.RegisterOliveTinApiServer(grpcServer, newServer())
grpcServer.Serve(lis)
err = grpcServer.Serve(lis)
if err != nil {
log.Fatalf("Could not start gRPC Server - %v", err)
}
}
func newServer() *oliveTinAPI {

View File

@@ -31,7 +31,6 @@ func startRestAPIServer(globalConfig *config.Config) error {
defer cancel()
// 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}))
mux := runtime.NewServeMux(
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
Marshaler: &runtime.JSONPb{
@@ -47,7 +46,9 @@ func startRestAPIServer(globalConfig *config.Config) error {
err := gw.RegisterOliveTinApiHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
if err != nil {
log.Fatalf("gw error %v", err)
log.Errorf("Could not register REST API Handler %v", err)
return err
}
return http.ListenAndServe(cfg.ListenAddressRestActions, cors.AllowCors(mux))

View File

@@ -24,11 +24,14 @@ func findWebuiDir() string {
directoriesToSearch := []string{
"./webui",
"/var/www/olivetin/",
"/etc/OliveTin/webui/",
}
for _, dir := range directoriesToSearch {
if _, err := os.Stat(dir); !os.IsNotExist(err) {
log.Infof("Found the webui directory here: %v", dir)
log.WithFields(log.Fields{
"dir": dir,
}).Infof("Found the webui directory")
return dir
}
@@ -55,7 +58,11 @@ func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
ShowNewVersions: cfg.ShowNewVersions,
})
w.Write([]byte(jsonRet))
_, err := w.Write([]byte(jsonRet))
if err != nil {
log.Warnf("Could not write webui settings: %v", err)
}
}
func startWebUIServer(cfg *config.Config) {

View File

@@ -2,10 +2,13 @@ package httpservers
import (
"github.com/stretchr/testify/assert"
"os"
"testing"
)
func TestGetWebuiDir(t *testing.T) {
os.Chdir("../../") // go test sets the cwd to "httpservers" by default
dir := findWebuiDir()
assert.Equal(t, "./webui", dir, "Finding the webui dir")

View File

@@ -21,7 +21,10 @@ type updateRequest struct {
MachineID string
}
// AvailableVersion is updated when checking with the update service.
var AvailableVersion = "none"
// CurrentVersion is set by the main cmd (which is in tern set as a compile constant)
var CurrentVersion = "?"
func machineID() string {
@@ -38,13 +41,13 @@ func machineID() string {
// StartUpdateChecker will start a job that runs periodically, checking
// for updates.
func StartUpdateChecker(currentVersion string, currentCommit string, cfg *config.Config) {
CurrentVersion = currentVersion
if !cfg.CheckForUpdates {
log.Warn("Update checking is disabled")
return
}
CurrentVersion = currentVersion
payload := updateRequest{
CurrentVersion: currentVersion,
CurrentCommit: currentCommit,

View File

@@ -3,10 +3,11 @@
package tools
import (
_ "github.com/bufbuild/buf/cmd/buf"
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
_ "github.com/bufbuild/buf/cmd/buf"
_ "github.com/fzipp/gocyclo/cmd/gocyclo"
_ "github.com/go-critic/go-critic/cmd/gocritic"
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
)

81
var/initscript/OliveTin Executable file
View File

@@ -0,0 +1,81 @@
#!/bin/bash
#
# olivetin Start/Stop OliveTin
#
# chkconfig: 2345 55 25
# description: SSH is a protocol for secure remote shell access. \
# This service starts up the OpenSSH server daemon.
#
# processname: OliveTin
### BEGIN INIT INFO
# Provides: OliveTin
# Required-Start: $local_fs $network $syslog
# Required-Stop: $local_fs $syslog
# Should-Start: $syslog
# Should-Stop: $network $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start/Stop OliveTin
# Description: OliveTin is an app to run your Linux shell commands from a web interface.
#
### END INIT INFO
# source function library
. /etc/rc.d/init.d/functions
RETVAL=0
prog="OliveTin"
lockfile=/var/lock/subsys/$prog
runlevel=$(set -- $(runlevel); eval "echo \$$#" )
start()
{
echo -n $"Starting $prog: "
/usr/local/bin/OliveTin $OPTIONS &
RETVAL=$?
return $RETVAL
}
stop()
{
echo -n $"Stopping $prog: "
killall OliveTin
RETVAL=$?
return $RETVAL
}
status() {
PID=$(pidof OliveTin)
RETVAL=$?
if [ $RETVAL -eq 1 ] ; then
echo "OliveTin is stopped"
else
echo "OliveTin is running"
fi
return $RETVAL
}
restart() {
stop
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
status)
status
;;
*)
echo $"Usage: $0 {start|stop|status|restart}"
RETVAL=2
esac
exit $RETVAL

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
webui/OliveTinLogo-57px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -8,17 +8,21 @@
<title>OliveTin</title>
<link rel = "stylesheet" type = "text/css" href = "style.css" />
<link rel = "shortcut icon" type = "image/png" href = "OliveTinLogo.png" />
<link rel = "apple-touch-icon" sizes="57x57" href="OliveTinLogo-57px.png" />
<link rel = "apple-touch-icon" sizes="120x120" href="OliveTinLogo-120px.png" />
<link rel = "apple-touch-icon" sizes="180x180" href="OliveTinLogo-180px.png" />
</head>
<body>
<main title = "main content">
<fieldset id = "switcher">
<fieldset id = "sectionSwitcher" title = "Sections">
<button id = "showActions">Actions</button>
<button id = "showLogs">Logs</button>
</fieldset>
<section id = "contentLogs" title = "Logs" hidden>
<table>
<table title = "Logs">
<thead>
<tr>
<th>Timestamp</th>
@@ -31,10 +35,15 @@
</section>
<section id = "contentActions" title = "Actions" hidden >
<fieldset id = "rootGroup">
<fieldset id = "rootGroup" title = "Dashboard of buttons">
</fieldset>
</section>
<noscript>
<div class = "error">Sorry, JavaScript is required to use OliveTin.</div>
</noscript>
</main>
<footer title = "footer">
<p><img title = "application icon" src = "OliveTinLogo.png" height = "1em" class = "logo" /> OliveTin</p>
<p>
@@ -46,24 +55,28 @@
</footer>
<template id = "tplArgumentForm">
<div class = "wrapper">
<div>
<span class = "icon" role = "icon"></span>
<h2>Argument form</h2>
</div>
<form class = "actionArguments">
<div class = "wrapper">
<div>
<span class = "icon" role = "icon"></span>
<h2>Argument form</h2>
</div>
<div class = "arguments"></div>
<div class = "arguments"></div>
<div class = "buttons">
<input name = "start" type = "submit" value = "Start">
<button name = "cancel">Cancel</button>
<div class = "buttons">
<input name = "start" type = "submit" value = "Start">
<button name = "cancel">Cancel</button>
</div>
</div>
</div>
<form>
</template>
<template id = "tplActionButton">
<span role = "icon" title = "button icon" class = "icon">&#x1f4a9;</span>
<p role = "title" class = "title">Untitled Button</p>
<button>
<span role = "icon" title = "button icon" class = "icon">&#x1f4a9;</span>
<p role = "title" class = "title">Untitled Button</p>
</button>
</template>
<template id = "tplLogRow">
@@ -92,6 +105,28 @@
</tr>
</template>
<script type = "text/javascript">
/**
This is the bootstrap code, which relies on very simple, old javascript
to at least display a helpful error message if we can't use OliveTin.
*/
function showBigError (type, friendlyType, message) {
clearInterval(window.buttonInterval)
console.error('Error ' + type + ': ', message)
const domErr = document.createElement('div')
domErr.classList.add('error')
domErr.innerHTML = '<h1>Error ' + friendlyType + '</h1><p>' + message + "</p><p><a href = 'http://docs.olivetin.app/troubleshooting.html' target = 'blank'/>OliveTin Documentation</a></p>"
document.getElementById('rootGroup').appendChild(domErr)
}
</script>
<script type = "text/javascript" nomodule>
showBigError("js-modules-not-supported", "Sorry, your browser does not support JavaScript modules.", null)
</script>
<script type = "module" src = "main.js"></script>
</body>
</html>

View File

@@ -1,20 +1,26 @@
import { marshalLogsJsonToHtml } from './marshaller.js'
import './ArgumentForm.js'
class ActionButton extends window.HTMLButtonElement {
class ActionButton extends window.HTMLElement {
constructFromJson (json) {
this.updateIterationTimestamp = 0
this.title = json.title
this.constructDomFromTemplate()
// Class attributes
this.temporaryStatusMessage = null
this.isWaiting = false
this.actionCallUrl = window.restBaseUrl + 'StartAction'
this.updateFromJson(json)
this.onclick = () => {
// DOM Attributes
this.setAttribute('role', 'none')
this.btn.title = json.title
this.btn.onclick = () => {
console.log(json.arguments)
if (json.arguments.length > 0) {
const frm = document.createElement('form', { is: 'argument-form' })
const frm = document.createElement('argument-form')
frm.setup(json, (args) => {
this.startAction(args)
})
@@ -25,9 +31,9 @@ class ActionButton extends window.HTMLButtonElement {
}
}
this.constructTemplate()
this.updateFromJson(json)
this.updateHtml()
this.updateDom()
this.setAttribute('id', 'actionButton_' + json.id)
}
@@ -47,17 +53,17 @@ class ActionButton extends window.HTMLButtonElement {
}
startAction (actionArgs) {
this.disabled = true
this.btn.disabled = true
this.isWaiting = true
this.updateHtml()
this.classList = [] // Removes old animation classes
this.updateDom()
this.btn.classList = [] // Removes old animation classes
if (actionArgs === undefined) {
actionArgs = []
}
const startActionArgs = {
actionName: this.title,
actionName: this.btn.title,
arguments: actionArgs
}
@@ -92,28 +98,29 @@ class ActionButton extends window.HTMLButtonElement {
}
onActionResult (cssClass, temporaryStatusMessage) {
this.btn.disabled = false
this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
this.updateHtml()
this.classList.add(cssClass)
this.updateDom()
this.btn.classList.add(cssClass)
setTimeout(() => {
this.classList.remove(cssClass)
this.btn.classList.remove(cssClass)
}, 1000)
}
onActionError (err) {
console.log('callback error', err)
this.disabled = false
console.error('callback error', err)
this.btn.disabled = false
this.isWaiting = false
this.updateHtml()
this.classList.add('actionFailed')
this.updateDom()
this.btn.classList.add('actionFailed')
setTimeout(() => {
this.classList.remove('actionFailed')
this.btn.classList.remove('actionFailed')
}, 1000)
}
constructTemplate () {
constructDomFromTemplate () {
const tpl = document.getElementById('tplActionButton')
const content = tpl.content.cloneNode(true)
@@ -124,11 +131,12 @@ class ActionButton extends window.HTMLButtonElement {
this.appendChild(content)
this.domTitle = this.querySelector('.title')
this.domIcon = this.querySelector('.icon')
this.btn = this.querySelector('button')
this.domTitle = this.btn.querySelector('.title')
this.domIcon = this.btn.querySelector('.icon')
}
updateHtml () {
updateDom () {
if (this.temporaryStatusMessage != null) {
this.domTitle.innerText = this.temporaryStatusMessage
this.domTitle.classList.add('temporaryStatusMessage')
@@ -138,16 +146,16 @@ class ActionButton extends window.HTMLButtonElement {
setTimeout(() => {
this.temporaryStatusMessage = null
this.domTitle.classList.remove('temporaryStatusMessage')
this.updateHtml()
this.updateDom()
}, 2000)
} else if (this.isWaiting) {
this.domTitle.innerText = 'Waiting...'
} else {
this.domTitle.innerText = this.title
this.domTitle.innerText = this.btn.title
}
this.domIcon.innerHTML = this.unicodeIcon
}
}
window.customElements.define('action-button', ActionButton, { extends: 'button' })
window.customElements.define('action-button', ActionButton)

View File

@@ -1,5 +1,5 @@
class ArgumentForm extends window.HTMLFormElement {
class ArgumentForm extends window.HTMLElement {
setup (json, callback) {
this.setAttribute('class', 'actionArguments')
@@ -140,4 +140,4 @@ class ArgumentForm extends window.HTMLFormElement {
}
}
window.customElements.define('argument-form', ArgumentForm, { extends: 'form' })
window.customElements.define('argument-form', ArgumentForm)

View File

@@ -7,19 +7,20 @@ export function marshalActionButtonsJsonToHtml (json) {
let htmlButton = document.querySelector('#actionButton_' + jsonButton.id)
if (htmlButton == null) {
htmlButton = document.createElement('button', { is: 'action-button' })
htmlButton = document.createElement('action-button')
htmlButton.constructFromJson(jsonButton)
document.getElementById('rootGroup').appendChild(htmlButton)
} else {
htmlButton.updateFromJson(jsonButton)
htmlButton.updateHtml()
htmlButton.updateDom()
}
htmlButton.updateIterationTimestamp = currentIterationTimestamp
}
for (const existingButton of document.querySelector('#contentActions').querySelectorAll('button')) {
// Remove existing, but stale buttons (that were not updated in this round)
for (const existingButton of document.querySelector('#contentActions').querySelectorAll('action-button')) {
if (existingButton.updateIterationTimestamp !== currentIterationTimestamp) {
existingButton.remove()
}

View File

@@ -2,18 +2,6 @@
import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
function showBigError (type, friendlyType, message) {
clearInterval(window.buttonInterval)
console.error('Error ' + type + ': ', message)
const domErr = document.createElement('div')
domErr.classList.add('error')
domErr.innerHTML = '<h1>Error ' + friendlyType + '</h1><p>' + message + "</p><p><a href = 'http://docs.olivetin.app/troubleshooting.html' target = 'blank'/>OliveTin Documentation</a></p>"
document.getElementById('rootGroup').appendChild(domErr)
}
function showSection (name) {
for (const otherName of ['Actions', 'Logs']) {
document.getElementById('show' + otherName).classList.remove('activeSection')
@@ -39,7 +27,7 @@ function fetchGetDashboardComponents () {
}).then(res => {
marshalActionButtonsJsonToHtml(res)
}).catch(err => {
showBigError('fetch-buttons', 'getting buttons', err, 'blat')
window.showBigError('fetch-buttons', 'getting buttons', err, 'blat')
})
}
@@ -51,7 +39,7 @@ function fetchGetLogs () {
}).then(res => {
marshalLogsJsonToHtml(res)
}).catch(err => {
showBigError('fetch-buttons', 'getting buttons', err, 'blat')
window.showBigError('fetch-buttons', 'getting buttons', err, 'blat')
})
}
@@ -74,20 +62,24 @@ function processWebuiSettingsJson (settings) {
document.querySelector('#availableVersion').hidden = false
}
document.querySelector('#switcher').hidden = settings.HideNavigation
document.querySelector('#sectionSwitcher').hidden = settings.HideNavigation
}
setupSections()
function main () {
setupSections()
window.fetch('webUiSettings.json').then(res => {
return res.json()
}).then(res => {
processWebuiSettingsJson(res)
window.fetch('webUiSettings.json').then(res => {
return res.json()
}).then(res => {
processWebuiSettingsJson(res)
fetchGetDashboardComponents()
fetchGetLogs()
fetchGetDashboardComponents()
fetchGetLogs()
window.buttonInterval = setInterval(fetchGetDashboardComponents, 3000)
}).catch(err => {
showBigError('fetch-webui-settings', 'getting webui settings', err)
})
window.buttonInterval = setInterval(fetchGetDashboardComponents, 3000)
}).catch(err => {
window.showBigError('fetch-webui-settings', 'getting webui settings', err)
})
}
main() // call self

View File

@@ -42,10 +42,6 @@ tr:hover td {
background-color: beige;
}
button.activeSection {
font-weight: bold;
}
legend {
padding-top: 1em;
}
@@ -90,11 +86,12 @@ div.entity h2 {
grid-column: 1 / span all;
}
/* Buttons */
button,
input[type="submit"] {
padding: 1em;
color: black;
display: table-cell;
text-align: center;
border: 1px solid #999;
background-color: white;
@@ -120,20 +117,43 @@ input[type="submit"]:disabled {
cursor: not-allowed;
}
fieldset#switcher {
fieldset#sectionSwitcher {
border: 0;
text-align: right;
margin-bottom: 1em;
}
fieldset#switcher button:first-child {
fieldset#sectionSwitcher button {
padding: 1em;
color: black;
display: table-cell;
text-align: center;
border: 1px solid #999;
background-color: white;
box-shadow: 0 0 6px 0 #aaa;
user-select: none;
cursor: pointer;
}
fieldset#rootGroup action-button button {
width: 100%;
height: 100%;
}
fieldset#sectionSwitcher button:first-child {
border-radius: 1em 0 0 1em;
}
fieldset#switcher button:last-child {
fieldset#sectionSwitcher button:last-child {
border-radius: 0 1em 1em 0;
}
button.activeSection {
font-weight: bold;
}
/* Button animations */
.actionFailed {
animation: kfActionFailed 1s;
}