Skip to content

Commit 88dd4e6

Browse files
authored
actions (#2)
Allow actions that execute shell commands to be configured in a TOML config file, and triggered via API request.
1 parent 6876c7b commit 88dd4e6

File tree

10 files changed

+160
-8
lines changed

10 files changed

+160
-8
lines changed

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ It currently does the following things:
77
- **Event log**: Services inside a TDX instance can record events they want exposed to the operator
88
used to record and query events. Useful to record service startup/shutdown, errors, progress updates,
99
hashes, etc.
10+
- **Actions**: Ability to execute shell commands via API
1011

1112
Future features:
1213

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

1717
---
1818

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

3131
# Query events (plain text or JSON is supported)
32-
$ curl -s localhost:3535/api/v1/events?format=text
32+
$ curl localhost:3535/api/v1/events?format=text
3333
2024-10-23T12:04:01Z hello world
3434
2024-10-23T12:04:07Z this is a test
3535
```
36+
37+
## Actions
38+
39+
Actions are shell commands that can be executed via API. The commands are defined in the config file,
40+
see [systemapi-config.toml](./systemapi-config.toml) for examples.
41+
42+
```bash
43+
# Start the server
44+
$ go run cmd/system-api/main.go --config systemapi-config.toml
45+
46+
# Execute the example action
47+
$ curl -v localhost:3535/api/v1/actions/echo_test
48+
```

cmd/system-api/main.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ var flags []cli.Flag = []cli.Flag{
3434
Value: true,
3535
Usage: "log debug messages",
3636
},
37+
&cli.StringFlag{
38+
Name: "config",
39+
Value: "",
40+
Usage: "config file",
41+
},
3742
}
3843

3944
func main() {
@@ -50,7 +55,7 @@ func main() {
5055
}
5156
}
5257

53-
func runCli(cCtx *cli.Context) error {
58+
func runCli(cCtx *cli.Context) (err error) {
5459
listenAddr := cCtx.String("listen-addr")
5560
pipeFile := cCtx.String("pipe-file")
5661
logJSON := cCtx.Bool("log-json")
@@ -59,6 +64,7 @@ func runCli(cCtx *cli.Context) error {
5964
logTags := map[string]string{
6065
"version": common.Version,
6166
}
67+
configFile := cCtx.String("config")
6268

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

77+
var config *systemapi.SystemAPIConfig
78+
if configFile != "" {
79+
config, err = systemapi.LoadConfigFromFile(configFile)
80+
if err != nil {
81+
log.Error("Error loading config", "err", err)
82+
return err
83+
}
84+
log.Info("Loaded config", "config-file", config)
85+
}
86+
7187
// Setup and start the server (in the background)
72-
server, err := systemapi.NewServer(&systemapi.HTTPServerConfig{
88+
cfg := &systemapi.HTTPServerConfig{
7389
ListenAddr: listenAddr,
7490
Log: log,
7591
PipeFilename: pipeFile,
76-
})
92+
Config: config,
93+
}
94+
server, err := systemapi.NewServer(cfg)
7795
if err != nil {
7896
return err
7997
}

common/utils.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"strconv"
66
)
77

8+
// GetEnvInt returns the value of the environment variable named by key, or defaultValue if the environment variable
9+
// doesn't exist or is not a valid integer
810
func GetEnvInt(key string, defaultValue int) int {
911
if value, ok := os.LookupEnv(key); ok {
1012
val, err := strconv.Atoi(value)
@@ -14,3 +16,11 @@ func GetEnvInt(key string, defaultValue int) int {
1416
}
1517
return defaultValue
1618
}
19+
20+
// GetEnv returns the value of the environment variable named by key, or defaultValue if the environment variable doesn't exist
21+
func GetEnv(key, defaultValue string) string {
22+
if value, ok := os.LookupEnv(key); ok {
23+
return value
24+
}
25+
return defaultValue
26+
}

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@ require (
88
github.com/ethereum/go-ethereum v1.14.9
99
github.com/go-chi/chi/v5 v5.1.0
1010
github.com/go-chi/httplog/v2 v2.1.1
11+
github.com/pelletier/go-toml/v2 v2.2.3
12+
github.com/stretchr/testify v1.9.0
1113
github.com/urfave/cli/v2 v2.27.2
1214
)
1315

1416
require (
1517
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
1618
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
19+
github.com/davecgh/go-spew v1.1.1 // indirect
1720
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
1821
github.com/holiman/uint256 v1.3.1 // indirect
22+
github.com/pmezard/go-difflib v1.0.0 // indirect
1923
github.com/russross/blackfriday/v2 v2.1.0 // indirect
2024
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
2125
golang.org/x/crypto v0.22.0 // indirect
2226
golang.org/x/sys v0.25.0 // indirect
27+
gopkg.in/yaml.v3 v3.0.1 // indirect
2328
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,14 @@ github.com/go-chi/httplog/v2 v2.1.1 h1:ojojiu4PIaoeJ/qAO4GWUxJqvYUTobeo7zmuHQJAx
1818
github.com/go-chi/httplog/v2 v2.1.1/go.mod h1:/XXdxicJsp4BA5fapgIC3VuTD+z0Z/VzukoB3VDc1YE=
1919
github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs=
2020
github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
21+
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
22+
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
23+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
24+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2125
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
2226
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
27+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
28+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
2329
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
2430
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
2531
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
@@ -28,3 +34,7 @@ golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
2834
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
2935
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
3036
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
37+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
38+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
39+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
40+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

systemapi-config.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[actions]
2+
# reboot = "reboot"
3+
# rbuilder_restart = "/etc/init.d/rbuilder restart"
4+
# rbuilder_stop = "/etc/init.d/rbuilder stop"
5+
echo_test = "echo test"

systemapi/config.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package systemapi
2+
3+
import (
4+
"os"
5+
6+
toml "github.com/pelletier/go-toml/v2"
7+
)
8+
9+
type SystemAPIConfig struct {
10+
Actions map[string]string
11+
}
12+
13+
func LoadConfigFromFile(path string) (*SystemAPIConfig, error) {
14+
content, err := os.ReadFile(path)
15+
if err != nil {
16+
return nil, err
17+
}
18+
return LoadConfig(content)
19+
}
20+
21+
func LoadConfig(content []byte) (*SystemAPIConfig, error) {
22+
cfg := &SystemAPIConfig{}
23+
err := toml.Unmarshal(content, cfg)
24+
if err != nil {
25+
return nil, err
26+
}
27+
return cfg, nil
28+
}

systemapi/config_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package systemapi
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestLoadConfig(t *testing.T) {
10+
path := "../systemapi-config.toml"
11+
cfg, err := LoadConfigFromFile(path)
12+
require.NoError(t, err)
13+
require.NotNil(t, cfg)
14+
require.NotEmpty(t, cfg.Actions)
15+
require.Equal(t, "echo test", cfg.Actions["echo_test"])
16+
}

systemapi/server.go

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,23 @@ package systemapi
33

44
import (
55
"bufio"
6+
"bytes"
67
"context"
78
"encoding/json"
89
"errors"
910
"net/http"
1011
"os"
12+
"os/exec"
1113
"strings"
1214
"sync"
1315
"syscall"
1416
"time"
1517

16-
"github.com/flashbots/system-api/common"
1718
chi "github.com/go-chi/chi/v5"
1819
"github.com/go-chi/chi/v5/middleware"
1920
"github.com/go-chi/httplog/v2"
2021
)
2122

22-
var MaxEvents = common.GetEnvInt("MAX_EVENTS", 1000)
23-
2423
type HTTPServerConfig struct {
2524
ListenAddr string
2625
Log *httplog.Logger
@@ -31,6 +30,8 @@ type HTTPServerConfig struct {
3130
GracefulShutdownDuration time.Duration
3231
ReadTimeout time.Duration
3332
WriteTimeout time.Duration
33+
34+
Config *SystemAPIConfig
3435
}
3536

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

9092
if s.cfg.EnablePprof {
9193
s.log.Info("pprof API enabled")
@@ -191,3 +193,40 @@ func (s *Server) handleGetEvents(w http.ResponseWriter, r *http.Request) {
191193
return
192194
}
193195
}
196+
197+
func (s *Server) handleAction(w http.ResponseWriter, r *http.Request) {
198+
action := chi.URLParam(r, "action")
199+
s.log.Info("Received action", "action", action)
200+
201+
if s.cfg.Config == nil {
202+
w.WriteHeader(http.StatusNotImplemented)
203+
return
204+
}
205+
206+
cmd, ok := s.cfg.Config.Actions[action]
207+
if !ok {
208+
w.WriteHeader(http.StatusNotImplemented)
209+
return
210+
}
211+
212+
s.log.Info("Executing action", "action", action, "cmd", cmd)
213+
stdout, stderr, err := Shellout(cmd)
214+
if err != nil {
215+
s.log.Error("Failed to execute action", "action", action, "cmd", cmd, "err", err, "stderr", stderr)
216+
w.WriteHeader(http.StatusInternalServerError)
217+
return
218+
}
219+
220+
s.log.Info("Action executed", "action", action, "cmd", cmd, "stdout", stdout, "stderr", stderr)
221+
w.WriteHeader(http.StatusOK)
222+
}
223+
224+
func Shellout(command string) (string, string, error) {
225+
var stdout bytes.Buffer
226+
var stderr bytes.Buffer
227+
cmd := exec.Command(ShellToUse, "-c", command)
228+
cmd.Stdout = &stdout
229+
cmd.Stderr = &stderr
230+
err := cmd.Run()
231+
return stdout.String(), stderr.String(), err
232+
}

systemapi/vars.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package systemapi
2+
3+
import "github.com/flashbots/system-api/common"
4+
5+
var (
6+
MaxEvents = common.GetEnvInt("MAX_EVENTS", 1000)
7+
ShellToUse = common.GetEnv("SHELL_TO_USE", "/bin/ash")
8+
)

0 commit comments

Comments
 (0)