Skip to content

Commit

Permalink
add support for running without a tty (#65)
Browse files Browse the repository at this point in the history
* abstract printing from pterm for non-tty mode

* run without TUI

* formatting

* update CMD for package to not render TUI

* being extraction of tui and loader

* Revert "run without TUI"

This reverts commit fdbcc55.

* remove tui managing loader lifecycle

* cleanup

* being abstracting out TUI as a watcher

* correctly handle sigint and lifecycle

* use waitgroups

* waitgroups in tui, tui uses loader

* waitgroup for tview run, fixup signals in run cmd

* cleanup run cmd

* cleanup tui watch()

* more cleanup

* Revert "cleanup tui watch()"

This reverts commit 391f359.

* comments for tui watch()

* remove printer abstraction

* code formatting

* implement no-tty mode

* move noopWatcher to constructor

* remove comment
  • Loading branch information
lgadban authored Mar 28, 2022
1 parent 06eaa22 commit 5636292
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 182 deletions.
15 changes: 6 additions & 9 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

BumbleBee by default uses a containerized build environment to build your BPF programs to an ELF file, then packages that in an OCI image according to our image spec.

You can then package your BPF program as standard Docker image that contains the `bee` CLI/runner in addition to your BPF programs.
Additionally, if desired, you can then package your BPF program as a standard Docker image that contains the `bee` CLI/runner in addition to your BPF programs.
The end result is a standard docker image that can be distributed via standard docker-like workflows to run your BPF program anywhere you run containerized workloads, such as a K8s cluster.
Note that you will need sufficient capabilities to run the image, as loading and running the BPF program is a privileged operation for most intents and purposes.

Expand All @@ -17,16 +17,13 @@ $ bee build examples/tcpconnect/tcpconnect.c tcpconnect
$ bee package tcpconnect bee-tcpconnect:latest
SUCCESS Packaged image built and tagged at bee-tcpconnect:latest

# run the bee-tcpconnect:latest image somewhere, deploy to K8s, etc.
# this example below runs locally via `docker` but see the following paragraph for a warning on weird terminal behavior when using this exact command!
$ docker run --privileged --tty bee-tcpconnect:latest
# run the bee-tcpconnect:latest image, deploy to K8s, etc.
$ docker run --privileged bee-tcpconnect:latest
```

Note that the `--privileged` flag is required to provide the permissions necessary and the `--tty` flag is necessary for the TUI rendered by default with `bee run`.
The `--tty` requirement will be removed shortly as we will introduce a mode that does not render the TUI.
Additionally, if you run the image as above, when you attempt to quit via <ctrl-c> your terminal may be left in a bad state. This is because the <ctrl-c> is being handled by `docker run` and not making it to the TTY.
To clear your screen, do a non-containerized run, e.g. `bee run ghcr.io/solo-io/bumblebee/tcpconnect:$(bee version)`.
Again, this will have a better UX very soon!
Note that the `--privileged` flag is used to provide the necessary permissions (alternatively this can be scoped down via capabilities through your system/orchestrator).
Since this will typically not be used interactively, by default the `CMD` for the container is `bee run --no-tty` which will not render the TUI.
Metrics can be scraped from this container to provide insight to your maps.

## BPF conventions

Expand Down
12 changes: 12 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,15 @@ You can then use the docker image in the `bee build` command, for example:
```bash
bee build examples/tcpconnect/tcpconnect.c tcpconnect.o -i $DOCKER_BUILT_IMAGE
```

### Workflow for `bee package` dev

```
TAGGED_VERSION=vdev make docker-build-bee
go run ./bee/main.go build -i ghcr.io/solo-io/bumblebee/builder:0.0.9 examples/tcpconnect/tcpconnect.c tcpconnect
go run ./bee/main.go package tcpconnect bee-tcpconnect:v1
docker run --privileged bee-tcpconnect:v1
```
2 changes: 1 addition & 1 deletion pkg/cli/internal/commands/package/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ COPY ./store /root/.bumblebee/store/

ARG BPF_IMAGE
ENV BPF_IMAGE=$BPF_IMAGE
CMD ./bee-linux-amd64 run ${BPF_IMAGE}
CMD ./bee-linux-amd64 run --no-tty ${BPF_IMAGE}
110 changes: 72 additions & 38 deletions pkg/cli/internal/commands/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type runOptions struct {

debug bool
filter []string
notty bool
}

const filterDescription string = "Filter to apply to output from maps. Format is \"map_name,key_name,regex\" " +
Expand All @@ -39,6 +40,7 @@ var stopper chan os.Signal
func addToFlags(flags *pflag.FlagSet, opts *runOptions) {
flags.BoolVarP(&opts.debug, "debug", "d", false, "Create a log file 'debug.log' that provides debug logs of loader and TUI execution")
flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, filterDescription)
flags.BoolVar(&opts.notty, "no-tty", false, "Set to true for running without a tty allocated, so no interaction will be expected or rich output will done")
}

func Command(opts *options.GeneralOptions) *cobra.Command {
Expand Down Expand Up @@ -76,22 +78,17 @@ $ bee run -f="events_hash,daddr,1.1.1.1" -f="events_ring,daddr,1.1.1.1" ghcr.io/
}

func run(cmd *cobra.Command, args []string, opts *runOptions) error {
// Subscribe to signals for terminating the program.
// This is used until management of signals is passed to the TUI
stopper = make(chan os.Signal, 1)
signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)
go func() {
for sig := range stopper {
if sig == os.Interrupt || sig == syscall.SIGTERM {
fmt.Println("got sigterm or interrupt")
os.Exit(0)
}
}
}()
ctx, err := buildContext(cmd.Context(), opts.debug)
if err != nil {
return err
}
contextutils.LoggerFrom(ctx).Info("starting bee run")
if opts.notty {
pterm.DisableStyling()
}

// guaranteed to be length 1
progLocation := args[0]
progReader, err := getProgram(cmd.Context(), opts.general, progLocation)
progReader, err := getProgram(ctx, opts.general, progLocation)
if err != nil {
return err
}
Expand All @@ -101,7 +98,7 @@ func run(cmd *cobra.Command, args []string, opts *runOptions) error {
return fmt.Errorf("could not raise memory limit (check for sudo or setcap): %v", err)
}

promProvider, err := stats.NewPrometheusMetricsProvider(cmd.Context(), &stats.PrometheusOpts{})
promProvider, err := stats.NewPrometheusMetricsProvider(ctx, &stats.PrometheusOpts{})
if err != nil {
return err
}
Expand All @@ -110,42 +107,51 @@ func run(cmd *cobra.Command, args []string, opts *runOptions) error {
decoder.NewDecoderFactory(),
promProvider,
)

parsedELF, err := progLoader.Parse(cmd.Context(), progReader)
parsedELF, err := progLoader.Parse(ctx, progReader)
if err != nil {
return fmt.Errorf("could not parse BPF program: %w", err)
}

// TODO: add filter to UI
filter, err := tui.BuildFilter(opts.filter, parsedELF.WatchedMaps)
tuiApp, err := buildTuiApp(&progLoader, progLocation, opts.filter, parsedELF)
if err != nil {
return fmt.Errorf("could not build filter %w", err)
return err
}
loaderOpts := loader.LoadOptions{
ParsedELF: parsedELF,
Watcher: tuiApp,
}

// bail out before starting TUI if context canceled
if ctx.Err() != nil {
contextutils.LoggerFrom(ctx).Info("before calling tui.Run() context is done")
return ctx.Err()
}
if opts.notty {
fmt.Println("Calling Load...")
loaderOpts.Watcher = loader.NewNoopWatcher()
err = progLoader.Load(ctx, &loaderOpts)
return err
} else {
contextutils.LoggerFrom(ctx).Info("calling tui run()")
err = tuiApp.Run(ctx, progLoader, &loaderOpts)
contextutils.LoggerFrom(ctx).Info("after tui run()")
return err
}
}

func buildTuiApp(loader *loader.Loader, progLocation string, filterString []string, parsedELF *loader.ParsedELF) (*tui.App, error) {
// TODO: add filter to UI
filter, err := tui.BuildFilter(filterString, parsedELF.WatchedMaps)
if err != nil {
return nil, fmt.Errorf("could not build filter %w", err)
}
appOpts := tui.AppOpts{
Loader: progLoader,
ProgLocation: progLocation,
ParsedELF: parsedELF,
Filter: filter,
}
app := tui.NewApp(&appOpts)

var sugaredLogger *zap.SugaredLogger
if opts.debug {
cfg := zap.NewDevelopmentConfig()
cfg.OutputPaths = []string{"debug.log"}
cfg.ErrorOutputPaths = []string{"debug.log"}
logger, err := cfg.Build()
if err != nil {
return fmt.Errorf("couldn't create zap logger: '%w'", err)
}
sugaredLogger = logger.Sugar()
} else {
sugaredLogger = zap.NewNop().Sugar()
}

ctx := contextutils.WithExistingLogger(cmd.Context(), sugaredLogger)
return app.Run(ctx, progReader)
return &app, nil
}

func getProgram(
Expand Down Expand Up @@ -202,3 +208,31 @@ func getProgram(

return progReader, nil
}

func buildContext(ctx context.Context, debug bool) (context.Context, error) {
ctx, cancel := context.WithCancel(ctx)
stopper = make(chan os.Signal, 1)
signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)
go func() {
<-stopper
fmt.Println("got sigterm or interrupt")
cancel()
}()

var sugaredLogger *zap.SugaredLogger
if debug {
cfg := zap.NewDevelopmentConfig()
cfg.OutputPaths = []string{"debug.log"}
cfg.ErrorOutputPaths = []string{"debug.log"}
logger, err := cfg.Build()
if err != nil {
return nil, fmt.Errorf("couldn't create zap logger: '%w'", err)
}
sugaredLogger = logger.Sugar()
} else {
sugaredLogger = zap.NewNop().Sugar()
}
ctx = contextutils.WithExistingLogger(ctx, sugaredLogger)

return ctx, nil
}
105 changes: 45 additions & 60 deletions pkg/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/cilium/ebpf/btf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/pterm/pterm"
"github.com/solo-io/bumblebee/pkg/decoder"
"github.com/solo-io/bumblebee/pkg/stats"
"github.com/solo-io/go-utils/contextutils"
Expand All @@ -35,24 +34,6 @@ type Loader interface {
Load(ctx context.Context, opts *LoadOptions) error
}

type KvPair struct {
Key map[string]string
Value string
Hash uint64
}

type MapEntry struct {
Name string
Entry KvPair
}

type MapWatcher interface {
NewRingBuf(name string, keys []string)
NewHashMap(name string, keys []string)
SendEntry(entry MapEntry)
PreWatchHandler()
}

type WatchedMap struct {
Name string
Labels []string
Expand Down Expand Up @@ -159,67 +140,72 @@ func (l *loader) Parse(ctx context.Context, progReader io.ReaderAt) (*ParsedELF,

func (l *loader) Load(ctx context.Context, opts *LoadOptions) error {
// TODO: add invariant checks on opts
loaderProgress, _ := pterm.DefaultSpinner.Start("Loading BPF program and maps into Kernel")
contextutils.LoggerFrom(ctx).Info("enter Load()")
// on shutdown notify watcher we have no more entries to send
defer opts.Watcher.Close()

// bail out before loading stuff into kernel if context canceled
if ctx.Err() != nil {
contextutils.LoggerFrom(ctx).Info("load entrypoint context is done")
return ctx.Err()
}

spec := opts.ParsedELF.Spec
// Load our eBPF spec into the kernel
coll, err := ebpf.NewCollection(spec)
if err != nil {
loaderProgress.Fail()
return err
}
defer coll.Close()
loaderProgress.Success()

linkerProgress, _ := pterm.DefaultSpinner.Start("Linking BPF functions to associated probe/tracepoint")
// For each program, add kprope/tracepoint
for name, prog := range spec.Programs {
switch prog.Type {
case ebpf.Kprobe:
var kp link.Link
var err error
if strings.HasPrefix(prog.SectionName, "kretprobe/") {
kp, err = link.Kretprobe(prog.AttachTo, coll.Programs[name])
if err != nil {
linkerProgress.Fail()
return fmt.Errorf("error attaching kretprobe '%v': %w", prog.Name, err)
}
} else {
kp, err = link.Kprobe(prog.AttachTo, coll.Programs[name])
if err != nil {
linkerProgress.Fail()
return fmt.Errorf("error attaching kprobe '%v': %w", prog.Name, err)
}
}
defer kp.Close()
case ebpf.TracePoint:
var tp link.Link
var err error
if strings.HasPrefix(prog.SectionName, "tracepoint/") {
tokens := strings.Split(prog.AttachTo, "/")
if len(tokens) != 2 {
return fmt.Errorf("unexpected tracepoint section '%v'", prog.AttachTo)
select {
case <-ctx.Done():
contextutils.LoggerFrom(ctx).Info("while loading progs context is done")
return ctx.Err()
default:
switch prog.Type {
case ebpf.Kprobe:
var kp link.Link
var err error
if strings.HasPrefix(prog.SectionName, "kretprobe/") {
kp, err = link.Kretprobe(prog.AttachTo, coll.Programs[name])
if err != nil {
return fmt.Errorf("error attaching kretprobe '%v': %w", prog.Name, err)
}
} else {
kp, err = link.Kprobe(prog.AttachTo, coll.Programs[name])
if err != nil {
return fmt.Errorf("error attaching kprobe '%v': %w", prog.Name, err)
}
}
tp, err = link.Tracepoint(tokens[0], tokens[1], coll.Programs[name])
if err != nil {
linkerProgress.Fail()
return fmt.Errorf("error attaching to tracepoint '%v': %w", prog.Name, err)
defer kp.Close()
case ebpf.TracePoint:
var tp link.Link
var err error
if strings.HasPrefix(prog.SectionName, "tracepoint/") {
tokens := strings.Split(prog.AttachTo, "/")
if len(tokens) != 2 {
return fmt.Errorf("unexpected tracepoint section '%v'", prog.AttachTo)
}
tp, err = link.Tracepoint(tokens[0], tokens[1], coll.Programs[name])
if err != nil {
return fmt.Errorf("error attaching to tracepoint '%v': %w", prog.Name, err)
}
}
defer tp.Close()
default:
return errors.New("only kprobe programs supported")
}
defer tp.Close()
default:
linkerProgress.Fail()
return errors.New("only kprobe programs supported")
}
}
linkerProgress.Success()

return l.watchMaps(ctx, opts.ParsedELF.WatchedMaps, coll, opts.Watcher)
}

func (l *loader) watchMaps(ctx context.Context, watchedMaps map[string]WatchedMap, coll *ebpf.Collection, watcher MapWatcher) error {
watcher.PreWatchHandler()

contextutils.LoggerFrom(ctx).Info("enter watchMaps()")
eg, ctx := errgroup.WithContext(ctx)
for name, bpfMap := range watchedMaps {
name := name
Expand Down Expand Up @@ -297,7 +283,6 @@ func (l *loader) startRingBuf(

for {
record, err := rd.Read()
logger.Info("read...")
if err != nil {
if errors.Is(err, ringbuf.ErrClosed) {
logger.Info("ringbuf closed...")
Expand Down
Loading

0 comments on commit 5636292

Please sign in to comment.