Skip to content

Commit

Permalink
actions (#2)
Browse files Browse the repository at this point in the history
Allow actions that execute shell commands to be configured in a TOML
config file, and triggered via API request.
  • Loading branch information
metachris authored Nov 4, 2024
1 parent 6876c7b commit 88dd4e6
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 8 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ It currently does the following things:
- **Event log**: Services inside a TDX instance can record events they want exposed to the operator
used to record and query events. Useful to record service startup/shutdown, errors, progress updates,
hashes, etc.
- **Actions**: Ability to execute shell commands via API

Future features:

- Operator can set a password for http-basic-auth (persisted, for all future requests)
- Operator-provided configuration (i.e. config values, secrets, etc.)
- Restart of services / execution of scripts

---

Expand All @@ -29,7 +29,20 @@ $ echo "hello world" > pipe.fifo
$ curl localhost:3535/api/v1/new_event?message=this+is+a+test

# Query events (plain text or JSON is supported)
$ curl -s localhost:3535/api/v1/events?format=text
$ curl localhost:3535/api/v1/events?format=text
2024-10-23T12:04:01Z hello world
2024-10-23T12:04:07Z this is a test
```

## Actions

Actions are shell commands that can be executed via API. The commands are defined in the config file,
see [systemapi-config.toml](./systemapi-config.toml) for examples.

```bash
# Start the server
$ go run cmd/system-api/main.go --config systemapi-config.toml

# Execute the example action
$ curl -v localhost:3535/api/v1/actions/echo_test
```
24 changes: 21 additions & 3 deletions cmd/system-api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ var flags []cli.Flag = []cli.Flag{
Value: true,
Usage: "log debug messages",
},
&cli.StringFlag{
Name: "config",
Value: "",
Usage: "config file",
},
}

func main() {
Expand All @@ -50,7 +55,7 @@ func main() {
}
}

func runCli(cCtx *cli.Context) error {
func runCli(cCtx *cli.Context) (err error) {
listenAddr := cCtx.String("listen-addr")
pipeFile := cCtx.String("pipe-file")
logJSON := cCtx.Bool("log-json")
Expand All @@ -59,6 +64,7 @@ func runCli(cCtx *cli.Context) error {
logTags := map[string]string{
"version": common.Version,
}
configFile := cCtx.String("config")

log := common.SetupLogger(&common.LoggingOpts{
JSON: logJSON,
Expand All @@ -68,12 +74,24 @@ func runCli(cCtx *cli.Context) error {
Tags: logTags,
})

var config *systemapi.SystemAPIConfig
if configFile != "" {
config, err = systemapi.LoadConfigFromFile(configFile)
if err != nil {
log.Error("Error loading config", "err", err)
return err
}
log.Info("Loaded config", "config-file", config)
}

// Setup and start the server (in the background)
server, err := systemapi.NewServer(&systemapi.HTTPServerConfig{
cfg := &systemapi.HTTPServerConfig{
ListenAddr: listenAddr,
Log: log,
PipeFilename: pipeFile,
})
Config: config,
}
server, err := systemapi.NewServer(cfg)
if err != nil {
return err
}
Expand Down
10 changes: 10 additions & 0 deletions common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"strconv"
)

// GetEnvInt returns the value of the environment variable named by key, or defaultValue if the environment variable
// doesn't exist or is not a valid integer
func GetEnvInt(key string, defaultValue int) int {
if value, ok := os.LookupEnv(key); ok {
val, err := strconv.Atoi(value)
Expand All @@ -14,3 +16,11 @@ func GetEnvInt(key string, defaultValue int) int {
}
return defaultValue
}

// GetEnv returns the value of the environment variable named by key, or defaultValue if the environment variable doesn't exist
func GetEnv(key, defaultValue string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return defaultValue
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@ require (
github.com/ethereum/go-ethereum v1.14.9
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/httplog/v2 v2.1.1
github.com/pelletier/go-toml/v2 v2.2.3
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v2 v2.27.2
)

require (
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/holiman/uint256 v1.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/sys v0.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ github.com/go-chi/httplog/v2 v2.1.1 h1:ojojiu4PIaoeJ/qAO4GWUxJqvYUTobeo7zmuHQJAx
github.com/go-chi/httplog/v2 v2.1.1/go.mod h1:/XXdxicJsp4BA5fapgIC3VuTD+z0Z/VzukoB3VDc1YE=
github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs=
github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
Expand All @@ -28,3 +34,7 @@ golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
5 changes: 5 additions & 0 deletions systemapi-config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[actions]
# reboot = "reboot"
# rbuilder_restart = "/etc/init.d/rbuilder restart"
# rbuilder_stop = "/etc/init.d/rbuilder stop"
echo_test = "echo test"
28 changes: 28 additions & 0 deletions systemapi/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package systemapi

import (
"os"

toml "github.com/pelletier/go-toml/v2"
)

type SystemAPIConfig struct {
Actions map[string]string
}

func LoadConfigFromFile(path string) (*SystemAPIConfig, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return LoadConfig(content)
}

func LoadConfig(content []byte) (*SystemAPIConfig, error) {
cfg := &SystemAPIConfig{}
err := toml.Unmarshal(content, cfg)
if err != nil {
return nil, err
}
return cfg, nil
}
16 changes: 16 additions & 0 deletions systemapi/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package systemapi

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestLoadConfig(t *testing.T) {
path := "../systemapi-config.toml"
cfg, err := LoadConfigFromFile(path)
require.NoError(t, err)
require.NotNil(t, cfg)
require.NotEmpty(t, cfg.Actions)
require.Equal(t, "echo test", cfg.Actions["echo_test"])
}
45 changes: 42 additions & 3 deletions systemapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@ package systemapi

import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"time"

"github.com/flashbots/system-api/common"
chi "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httplog/v2"
)

var MaxEvents = common.GetEnvInt("MAX_EVENTS", 1000)

type HTTPServerConfig struct {
ListenAddr string
Log *httplog.Logger
Expand All @@ -31,6 +30,8 @@ type HTTPServerConfig struct {
GracefulShutdownDuration time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration

Config *SystemAPIConfig
}

type Event struct {
Expand Down Expand Up @@ -86,6 +87,7 @@ func (s *Server) getRouter() http.Handler {
mux.Get("/livez", s.handleLivenessCheck)
mux.Get("/api/v1/new_event", s.handleNewEvent)
mux.Get("/api/v1/events", s.handleGetEvents)
mux.Get("/api/v1/actions/{action}", s.handleAction)

if s.cfg.EnablePprof {
s.log.Info("pprof API enabled")
Expand Down Expand Up @@ -191,3 +193,40 @@ func (s *Server) handleGetEvents(w http.ResponseWriter, r *http.Request) {
return
}
}

func (s *Server) handleAction(w http.ResponseWriter, r *http.Request) {
action := chi.URLParam(r, "action")
s.log.Info("Received action", "action", action)

if s.cfg.Config == nil {
w.WriteHeader(http.StatusNotImplemented)
return
}

cmd, ok := s.cfg.Config.Actions[action]
if !ok {
w.WriteHeader(http.StatusNotImplemented)
return
}

s.log.Info("Executing action", "action", action, "cmd", cmd)
stdout, stderr, err := Shellout(cmd)
if err != nil {
s.log.Error("Failed to execute action", "action", action, "cmd", cmd, "err", err, "stderr", stderr)
w.WriteHeader(http.StatusInternalServerError)
return
}

s.log.Info("Action executed", "action", action, "cmd", cmd, "stdout", stdout, "stderr", stderr)
w.WriteHeader(http.StatusOK)
}

func Shellout(command string) (string, string, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command(ShellToUse, "-c", command)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
return stdout.String(), stderr.String(), err
}
8 changes: 8 additions & 0 deletions systemapi/vars.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package systemapi

import "github.com/flashbots/system-api/common"

var (
MaxEvents = common.GetEnvInt("MAX_EVENTS", 1000)
ShellToUse = common.GetEnv("SHELL_TO_USE", "/bin/ash")
)

0 comments on commit 88dd4e6

Please sign in to comment.