Skip to content

Commit a1bb243

Browse files
authored
Add additional options for build and run (#89)
1 parent db2c8fe commit a1bb243

File tree

6 files changed

+205
-35
lines changed

6 files changed

+205
-35
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ _output
2121
# Staging ground for bpf programs
2222
bpf/
2323

24-
# Ignore vscode
24+
# Ignore IDE folders
2525
.vscode/
26-
.vagrant/
26+
.vagrant/
27+
.idea

builder/build.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#! /bin/bash
2-
set -eu
2+
set -eux
33

4-
clang-13 -g -O2 -target bpf -D__TARGET_ARCH_x86 -Wall -c $1 -o $2
4+
CFLAGS=${CFLAGS:-}
5+
6+
clang-13 -g -O2 -target bpf -D__TARGET_ARCH_x86 ${CFLAGS} -Wall -c $1 -o $2
57

68
# strip debug sections (see: https://github.com/libbpf/libbpf-bootstrap/blob/94000ca67c5e7be4741c09c435c9ae1777822378/examples/c/Makefile#L65)
79
llvm-strip-13 -g $2

pkg/cli/internal/commands/build/build.go

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,51 @@ import (
1212

1313
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1414
"github.com/pterm/pterm"
15+
"github.com/spf13/cobra"
16+
"github.com/spf13/pflag"
17+
"oras.land/oras-go/pkg/content"
18+
1519
"github.com/solo-io/bumblebee/builder"
1620
"github.com/solo-io/bumblebee/pkg/cli/internal/options"
1721
"github.com/solo-io/bumblebee/pkg/internal/version"
1822
"github.com/solo-io/bumblebee/pkg/spec"
19-
"github.com/spf13/cobra"
20-
"github.com/spf13/pflag"
21-
"oras.land/oras-go/pkg/content"
2223
)
2324

2425
type buildOptions struct {
25-
BuildImage string
26-
Builder string
27-
OutputFile string
26+
BuildImage string
27+
Builder string
28+
OutputFile string
2829
Local bool
30+
BinaryOnly bool
31+
CFlags []string
32+
BuildScript string
33+
BuildScriptOutput bool
2934

3035
general *options.GeneralOptions
3136
}
3237

38+
func (opts *buildOptions) validate() error {
39+
if !opts.Local {
40+
if opts.BuildScript != "" {
41+
fmt.Println("ignoring specified build script for docker build")
42+
}
43+
if opts.BuildScriptOutput {
44+
return fmt.Errorf("cannot write build script output for docker build")
45+
}
46+
}
47+
48+
return nil
49+
}
50+
3351
func addToFlags(flags *pflag.FlagSet, opts *buildOptions) {
3452
flags.StringVarP(&opts.BuildImage, "build-image", "i", fmt.Sprintf("ghcr.io/solo-io/bumblebee/builder:%s", version.Version), "Build image to use when compiling BPF program")
3553
flags.StringVarP(&opts.Builder, "builder", "b", "docker", "Executable to use for docker build command, default: `docker`")
3654
flags.StringVarP(&opts.OutputFile, "output-file", "o", "", "Output file for BPF ELF. If left blank will default to <inputfile.o>")
37-
flags.BoolVarP(&opts.Local, "local", "l", false, "Build the output binary and OCI image using local tools")
55+
flags.BoolVarP(&opts.Local, "local", "l", false, "Build the output binary using local tools")
56+
flags.StringVar(&opts.BuildScript, "build-script", "", "Optional path to a build script for building BPF program locally")
57+
flags.BoolVar(&opts.BuildScriptOutput, "build-script-out", false, "Print local script bee will use to build the BPF program")
58+
flags.BoolVar(&opts.BinaryOnly, "binary-only", false, "Only create output binary and do not package it into an OCI image")
59+
flags.StringArrayVar(&opts.CFlags, "cflags", nil, "cflags to be used when compiling the BPF program, passed as environment variable 'CFLAGS'")
3860
}
3961

4062
func Command(opts *options.GeneralOptions) *cobra.Command {
@@ -52,8 +74,22 @@ The bee build command has 2 main parts
5274
By default building is done in a docker container, however, this can be switched to local by adding the local flag:
5375
$ build INPUT_FILE REGISTRY_REF --local
5476
77+
If you would prefer to build only the object file, you can include the '--binary-only' flag,
78+
in which case you do not need to specify a registry:
79+
$ build INPUT_FILE --binary-only
80+
81+
Examples:
82+
You can specify multiple cflags with either a space separated string, or with multiple instances:
83+
$ build INPUT_FILE REGISTRY_REF --cflags="-DDEBUG -DDEBUG2"
84+
$ build INPUT_FILE REGISTRY_REF --cflags=-DDEBUG --cflags=-DDEBUG2
85+
86+
You can specify your own build script for building the BPF program locally.
87+
the input file will be passed as argument '$1' and the output filename as '$2'.
88+
Use the '--build-script-out' flag to see the default build script bee uses:
89+
$ build INPUT_FILE REGISTRY_REF --local --build-script-out
90+
$ build INPUT_FILE REGISTRY_REF --local --build-script=build.sh
5591
`,
56-
Args: cobra.ExactArgs(2),
92+
Args: cobra.RangeArgs(1, 2),
5793
RunE: func(cmd *cobra.Command, args []string) error {
5894
return build(cmd.Context(), args, buildOpts)
5995
},
@@ -69,6 +105,9 @@ $ build INPUT_FILE REGISTRY_REF --local
69105
}
70106

71107
func build(ctx context.Context, args []string, opts *buildOptions) error {
108+
if err := opts.validate(); err != nil {
109+
return err
110+
}
72111

73112
inputFile := args[0]
74113
outputFile := opts.OutputFile
@@ -99,8 +138,17 @@ func build(ctx context.Context, args []string, opts *buildOptions) error {
99138
// Create and start a fork of the default spinner.
100139
var buildSpinner *pterm.SpinnerPrinter
101140
if opts.Local {
141+
buildScript, err := getBuildScript(opts.BuildScript)
142+
if err != nil {
143+
return fmt.Errorf("could not load build script: %v", err)
144+
}
145+
if opts.BuildScriptOutput {
146+
fmt.Printf("%s\n", buildScript)
147+
return nil
148+
}
149+
102150
buildSpinner, _ = pterm.DefaultSpinner.Start("Compiling BPF program locally")
103-
if err := buildLocal(ctx, inputFile, outputFile); err != nil {
151+
if err := buildLocal(ctx, opts, buildScript, inputFile, outputFile); err != nil {
104152
buildSpinner.UpdateText("Failed to compile BPF program locally")
105153
buildSpinner.Fail()
106154
return err
@@ -116,6 +164,14 @@ func build(ctx context.Context, args []string, opts *buildOptions) error {
116164
buildSpinner.UpdateText(fmt.Sprintf("Successfully compiled \"%s\" and wrote it to \"%s\"", inputFile, outputFile))
117165
buildSpinner.Success() // Resolve spinner with success message.
118166

167+
if opts.BinaryOnly {
168+
return nil
169+
}
170+
171+
if len(args) == 1 {
172+
return fmt.Errorf("must specify a registry to package the output or run with '--binary-only'")
173+
}
174+
119175
// TODO: Figure out this hack, file.Seek() didn't seem to work
120176
outputFd.Close()
121177
reopened, err := os.Open(outputFile)
@@ -175,6 +231,18 @@ func getPlatformInfo(ctx context.Context) *ocispec.Platform {
175231
}
176232
}
177233

234+
func getBuildScript(path string) ([]byte, error) {
235+
if path != "" {
236+
buildScript, err := os.ReadFile(path)
237+
if err != nil {
238+
return nil, fmt.Errorf("could not read build script: %v", err)
239+
}
240+
return buildScript, nil
241+
}
242+
243+
return builder.GetBuildScript(), nil
244+
}
245+
178246
func buildDocker(
179247
ctx context.Context,
180248
opts *buildOptions,
@@ -190,10 +258,13 @@ func buildDocker(
190258
"run",
191259
"-v",
192260
fmt.Sprintf("%s:/usr/src/bpf", wd),
193-
opts.BuildImage,
194-
inputFile,
195-
outputFile,
196261
}
262+
263+
if len(opts.CFlags) > 0 {
264+
dockerArgs = append(dockerArgs, "--env", fmt.Sprintf("CFLAGS=%s", strings.Join(opts.CFlags, " ")))
265+
}
266+
dockerArgs = append(dockerArgs, opts.BuildImage, inputFile, outputFile)
267+
197268
dockerCmd := exec.CommandContext(ctx, opts.Builder, dockerArgs...)
198269
byt, err := dockerCmd.CombinedOutput()
199270
if err != nil {
@@ -203,12 +274,19 @@ func buildDocker(
203274
return nil
204275
}
205276

206-
func buildLocal(ctx context.Context, inputFile, outputFile string) error {
207-
buildScript := builder.GetBuildScript()
208-
277+
func buildLocal(
278+
ctx context.Context,
279+
opts *buildOptions,
280+
buildScript []byte,
281+
inputFile,
282+
outputFile string,
283+
) error {
209284
// Pass the script into sh via stdin, then arguments
210285
// TODO: need to handle CWD gracefully
211286
shCmd := exec.CommandContext(ctx, "sh", "-s", "--", inputFile, outputFile)
287+
shCmd.Env = []string{
288+
fmt.Sprintf("CFLAGS=%s", strings.Join(opts.CFlags, " ")),
289+
}
212290
stdin, err := shCmd.StdinPipe()
213291
if err != nil {
214292
return err
@@ -220,8 +298,8 @@ func buildLocal(ctx context.Context, inputFile, outputFile string) error {
220298
}()
221299

222300
out, err := shCmd.CombinedOutput()
301+
pterm.Info.Printf("%s\n", out)
223302
if err != nil {
224-
fmt.Printf("%s\n", out)
225303
return err
226304
}
227305
return nil

pkg/cli/internal/commands/run/run.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ import (
2727
type runOptions struct {
2828
general *options.GeneralOptions
2929

30-
debug bool
31-
filter []string
32-
notty bool
30+
debug bool
31+
filter []string
32+
notty bool
33+
pinMaps string
34+
pinProgs string
3335
}
3436

3537
const filterDescription string = "Filter to apply to output from maps. Format is \"map_name,key_name,regex\" " +
@@ -41,6 +43,8 @@ func addToFlags(flags *pflag.FlagSet, opts *runOptions) {
4143
flags.BoolVarP(&opts.debug, "debug", "d", false, "Create a log file 'debug.log' that provides debug logs of loader and TUI execution")
4244
flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, filterDescription)
4345
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")
46+
flags.StringVar(&opts.pinMaps, "pin-maps", "", "Directory to pin maps to, left unpinned if empty")
47+
flags.StringVar(&opts.pinProgs, "pin-progs", "", "Directory to pin progs to, left unpinned if empty")
4448
}
4549

4650
func Command(opts *options.GeneralOptions) *cobra.Command {
@@ -119,6 +123,8 @@ func run(cmd *cobra.Command, args []string, opts *runOptions) error {
119123
loaderOpts := loader.LoadOptions{
120124
ParsedELF: parsedELF,
121125
Watcher: tuiApp,
126+
PinMaps: opts.pinMaps,
127+
PinProgs: opts.pinProgs,
122128
}
123129

124130
// bail out before starting TUI if context canceled

pkg/loader/loader.go

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ import (
66
"fmt"
77
"io"
88
"log"
9+
"os"
10+
"path/filepath"
911
"strings"
1012
"time"
1113

1214
"github.com/cilium/ebpf"
1315
"github.com/cilium/ebpf/btf"
1416
"github.com/cilium/ebpf/link"
1517
"github.com/cilium/ebpf/ringbuf"
18+
"golang.org/x/sync/errgroup"
19+
1620
"github.com/solo-io/bumblebee/pkg/decoder"
1721
"github.com/solo-io/bumblebee/pkg/stats"
1822
"github.com/solo-io/go-utils/contextutils"
19-
"golang.org/x/sync/errgroup"
2023
)
2124

2225
type ParsedELF struct {
@@ -27,11 +30,14 @@ type ParsedELF struct {
2730
type LoadOptions struct {
2831
ParsedELF *ParsedELF
2932
Watcher MapWatcher
33+
PinMaps string
34+
PinProgs string
3035
}
3136

3237
type Loader interface {
3338
Parse(ctx context.Context, reader io.ReaderAt) (*ParsedELF, error)
3439
Load(ctx context.Context, opts *LoadOptions) error
40+
WatchMaps(ctx context.Context, watchedMaps map[string]WatchedMap, coll map[string]*ebpf.Map, watcher MapWatcher) error
3541
}
3642

3743
type WatchedMap struct {
@@ -88,6 +94,12 @@ func (l *loader) Parse(ctx context.Context, progReader io.ReaderAt) (*ParsedELF,
8894
return nil, err
8995
}
9096

97+
for _, prog := range spec.Programs {
98+
if prog.Type == ebpf.UnspecifiedProgram {
99+
contextutils.LoggerFrom(ctx).Debug("Program %s does not specify a type", prog.Name)
100+
}
101+
}
102+
91103
watchedMaps := make(map[string]WatchedMap)
92104
for name, mapSpec := range spec.Maps {
93105
if !isTrackedMap(mapSpec) {
@@ -150,9 +162,26 @@ func (l *loader) Load(ctx context.Context, opts *LoadOptions) error {
150162
return ctx.Err()
151163
}
152164

165+
if opts.PinMaps != "" {
166+
// Specify that we'd like to pin the referenced maps, or open them if already existing.
167+
for _, m := range opts.ParsedELF.Spec.Maps {
168+
// Do not pin/load read-only data
169+
if strings.HasSuffix(m.Name, ".rodata") {
170+
continue
171+
}
172+
173+
// PinByName specifies that we should pin the map by name, or load it if it already exists.
174+
m.Pinning = ebpf.PinByName
175+
}
176+
}
177+
153178
spec := opts.ParsedELF.Spec
154179
// Load our eBPF spec into the kernel
155-
coll, err := ebpf.NewCollection(spec)
180+
coll, err := ebpf.NewCollectionWithOptions(opts.ParsedELF.Spec, ebpf.CollectionOptions{
181+
Maps: ebpf.MapOptions{
182+
PinPath: opts.PinMaps,
183+
},
184+
})
156185
if err != nil {
157186
return err
158187
}
@@ -198,13 +227,29 @@ func (l *loader) Load(ctx context.Context, opts *LoadOptions) error {
198227
default:
199228
return errors.New("only kprobe programs supported")
200229
}
230+
if opts.PinProgs != "" {
231+
if err := createDir(ctx, opts.PinProgs, 0700); err != nil {
232+
return err
233+
}
234+
235+
pinFile := filepath.Join(opts.PinProgs, prog.Name)
236+
if err := coll.Programs[name].Pin(pinFile); err != nil {
237+
return fmt.Errorf("could not pin program '%s': %v", prog.Name, err)
238+
}
239+
fmt.Printf("Successfully pinned program '%v'\n", pinFile)
240+
}
201241
}
202242
}
203243

204-
return l.watchMaps(ctx, opts.ParsedELF.WatchedMaps, coll, opts.Watcher)
244+
return l.WatchMaps(ctx, opts.ParsedELF.WatchedMaps, coll.Maps, opts.Watcher)
205245
}
206246

207-
func (l *loader) watchMaps(ctx context.Context, watchedMaps map[string]WatchedMap, coll *ebpf.Collection, watcher MapWatcher) error {
247+
func (l *loader) WatchMaps(
248+
ctx context.Context,
249+
watchedMaps map[string]WatchedMap,
250+
maps map[string]*ebpf.Map,
251+
watcher MapWatcher,
252+
) error {
208253
contextutils.LoggerFrom(ctx).Info("enter watchMaps()")
209254
eg, ctx := errgroup.WithContext(ctx)
210255
for name, bpfMap := range watchedMaps {
@@ -221,7 +266,7 @@ func (l *loader) watchMaps(ctx context.Context, watchedMaps map[string]WatchedMa
221266
}
222267
eg.Go(func() error {
223268
watcher.NewRingBuf(name, bpfMap.Labels)
224-
return l.startRingBuf(ctx, bpfMap.valueStruct, coll.Maps[name], increment, name, watcher)
269+
return l.startRingBuf(ctx, bpfMap.valueStruct, maps[name], increment, name, watcher)
225270
})
226271
case ebpf.Array:
227272
fallthrough
@@ -238,7 +283,7 @@ func (l *loader) watchMaps(ctx context.Context, watchedMaps map[string]WatchedMa
238283
eg.Go(func() error {
239284
// TODO: output type of instrument in UI?
240285
watcher.NewHashMap(name, labelKeys)
241-
return l.startHashMap(ctx, bpfMap.mapSpec, coll.Maps[name], instrument, name, watcher)
286+
return l.startHashMap(ctx, bpfMap.mapSpec, maps[name], instrument, name, watcher)
242287
})
243288
default:
244289
// TODO: Support more map types
@@ -407,3 +452,17 @@ func (n *noop) Set(
407452
labels map[string]string,
408453
) {
409454
}
455+
456+
func createDir(ctx context.Context, path string, perm os.FileMode) error {
457+
file, err := os.Stat(path)
458+
if os.IsNotExist(err) {
459+
contextutils.LoggerFrom(ctx).Info("path does not exist, creating pin directory: %s", path)
460+
return os.Mkdir(path, perm)
461+
} else if err != nil {
462+
return fmt.Errorf("could not create pin directory '%v': %w", path, err)
463+
} else if !file.IsDir() {
464+
return fmt.Errorf("pin location '%v' exists but is not a directory", path)
465+
}
466+
467+
return nil
468+
}

0 commit comments

Comments
 (0)