Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

actions #2

Merged
merged 1 commit into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
)
Loading