Skip to content

Commit

Permalink
Introduce package command (#58)
Browse files Browse the repository at this point in the history
* introduce "uber image" builds

* full uber builds work

* clean up uber build

* ci for bee image

* add package command

* remove uber flag from bee build

* cleanup

* update docs, cleanup

* cleanup

* rename package
  • Loading branch information
lgadban authored Jan 14, 2022
1 parent 436c415 commit 12a3887
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 4 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,33 @@ jobs:
env:
TAGGED_VERSION: ${{ github.event.release.tag_name }}

push-bee-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Log in to the Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.PUSH_TOKEN }}
- name: Push bee container
run: make docker-push-bee
env:
TAGGED_VERSION: ${{ github.event.release.tag_name }}

push-example-programs:
runs-on: ubuntu-latest
permissions:
Expand Down
15 changes: 13 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ OUTDIR?=_output
HUB?=ghcr.io/solo-io
REPO_NAME?=bumblebee
EXAMPLES_DIR?=examples

DOCKER := docker

RELEASE := "true"
ifeq ($(TAGGED_VERSION),)
Expand All @@ -31,7 +31,6 @@ clean:
#----------------------------------------------------------------------------------
PUSH_CMD:=
PLATFORMS?=linux/amd64
DOCKER := docker
docker-build:
# may run into issues with apt-get and the apt.llvm.org repo, in which case use --no-cache to build
# e.g. `docker build --no-cache ./builder -f builder/Dockerfile -t $(HUB)/bumblebee/builder:$(VERSION)
Expand Down Expand Up @@ -87,6 +86,18 @@ build-cli: bee-linux-amd64 bee-linux-arm64
install-cli:
CGO_ENABLED=0 go install -ldflags=$(LDFLAGS) -gcflags=$(GCFLAGS) ./bee

BEE_DIR := bee
$(OUTDIR)/Dockerfile-bee: $(BEE_DIR)/Dockerfile-bee
cp $< $@

.PHONY: docker-build-bee
docker-build-bee: build-cli $(OUTDIR)/Dockerfile-bee
$(DOCKER) build $(OUTDIR) -f $(OUTDIR)/Dockerfile-bee -t $(HUB)/bumblebee/bee:$(VERSION)

.PHONY: docker-push-bee
docker-push-bee: docker-build-bee
$(DOCKER) push $(HUB)/bumblebee/bee:$(VERSION)

##----------------------------------------------------------------------------------
## Release
##----------------------------------------------------------------------------------
Expand Down
8 changes: 8 additions & 0 deletions bee/Dockerfile-bee
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM alpine:3.14

# installs public root certs
RUN apk upgrade --update-cache \
&& apk add ca-certificates \
&& rm -rf /var/cache/apk/*

ADD bee-linux-amd64 .
28 changes: 28 additions & 0 deletions docs/concepts.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Concepts

## Building

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.
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.

An example workflow is as follows:
```bash
$ bee build examples/tcpconnect/tcpconnect.c tcpconnect
SUCCESS Successfully compiled "examples/tcpconnect/tcpconnect.c" and wrote it to "examples/tcpconnect/tcpconnect.o"
SUCCESS Saved BPF OCI image to 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
```

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!

## BPF conventions

`BPF` programs are typically made up of 2 main parts:
Expand Down
2 changes: 2 additions & 0 deletions pkg/cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/solo-io/bumblebee/pkg/cli/internal/commands/initialize"
"github.com/solo-io/bumblebee/pkg/cli/internal/commands/list"
"github.com/solo-io/bumblebee/pkg/cli/internal/commands/login"
package_cmd "github.com/solo-io/bumblebee/pkg/cli/internal/commands/package"
"github.com/solo-io/bumblebee/pkg/cli/internal/commands/pull"
"github.com/solo-io/bumblebee/pkg/cli/internal/commands/push"
"github.com/solo-io/bumblebee/pkg/cli/internal/commands/run"
Expand Down Expand Up @@ -38,6 +39,7 @@ func Bee() *cobra.Command {

cmd.AddCommand(
build.Command(opts),
package_cmd.Command(opts),
run.Command(opts),
initialize.Command(),
push.Command(opts),
Expand Down
2 changes: 0 additions & 2 deletions pkg/cli/internal/commands/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ func addToFlags(flags *pflag.FlagSet, opts *buildOptions) {
flags.StringVarP(&opts.Builder, "builder", "b", "docker", "Executable to use for docker build command, default: `docker`")
flags.StringVarP(&opts.OutputFile, "output-file", "o", "", "Output file for BPF ELF. If left blank will default to <inputfile.o>")
flags.BoolVarP(&opts.Local, "local", "l", false, "Build the output binary and OCI image using local tools")

}

func Command(opts *options.GeneralOptions) *cobra.Command {
Expand Down Expand Up @@ -181,7 +180,6 @@ func buildDocker(
opts *buildOptions,
inputFile, outputFile string,
) error {
// TODO: handle cwd to be glooBPF/epfctl?
// TODO: debug log this
wd, err := os.Getwd()
if err != nil {
Expand Down
10 changes: 10 additions & 0 deletions pkg/cli/internal/commands/package/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ARG BEE_IMAGE

FROM $BEE_IMAGE

USER root
COPY ./store /root/.bumblebee/store/

ARG BPF_IMAGE
ENV BPF_IMAGE=$BPF_IMAGE
CMD ./bee-linux-amd64 run ${BPF_IMAGE}
151 changes: 151 additions & 0 deletions pkg/cli/internal/commands/package/package.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package package_cmd

import (
"context"
_ "embed"
"fmt"
"os"
"os/exec"

"github.com/pterm/pterm"
"github.com/solo-io/bumblebee/pkg/cli/internal/options"
"github.com/solo-io/bumblebee/pkg/internal/version"
"github.com/solo-io/bumblebee/pkg/spec"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"oras.land/oras-go/pkg/content"
"oras.land/oras-go/pkg/oras"
)

//go:embed Dockerfile
var packagedDockerfile []byte

type buildOptions struct {
BeeImage string
Builder string

general *options.GeneralOptions
}

func addToFlags(flags *pflag.FlagSet, opts *buildOptions) {
flags.StringVarP(&opts.Builder, "builder", "b", "docker", "Executable to use for docker build command")
flags.StringVar(&opts.BeeImage, "bee-image", "ghcr.io/solo-io/bumblebee/bee:"+version.Version, "Docker image (including tag) to use a base image for packaged image")
}

func Command(opts *options.GeneralOptions) *cobra.Command {
buildOpts := &buildOptions{
general: opts,
}
cmd := &cobra.Command{
Use: "package REGISTRY_REF DOCKER_IMAGE",
Short: "Package a BPF program OCI image with the `bee` runner in a docker image",
Long: `
The package command is used to package the desired BPF program along with the 'bee' runner in a Docker image.
This means that the resulting docker image is a single, runnable unit to load and attach your BPF proograms.
You can then ship this image around anywhere you run docker images, e.g. K8s.
Example workflow:
$ bee build examples/tcpconnect/tcpconnect.c tcpconnect
$ bee package tcpconnect bee-tcpconnect:latest
# deploy 'bee-tcpconnect:latest' to K8s cluster
`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return build(cmd.Context(), args, buildOpts)
},
SilenceUsage: true, // Usage on error is bad
}

cmd.OutOrStdout()

// Init flags
addToFlags(cmd.PersistentFlags(), buildOpts)

return cmd
}

func build(ctx context.Context, args []string, opts *buildOptions) error {

reg, err := content.NewOCI(opts.general.OCIStorageDir)
if err != nil {
return err
}
registryRef := args[0]

packagingSpinner, _ := pterm.DefaultSpinner.Start("Packaging BPF and bee image")
tmpDir, _ := os.MkdirTemp("", "bee_oci_store")
tmpStore := tmpDir + "/store"
err = os.Mkdir(tmpStore, 0755)
if err != nil {
packagingSpinner.UpdateText(fmt.Sprintf("Failed to create temp dir: %s", tmpStore))
packagingSpinner.Fail()
return err
}
if opts.general.Verbose {
fmt.Println("Temp dir name:", tmpDir)
fmt.Println("Temp store:", tmpStore)
}
defer os.RemoveAll(tmpDir)

tempReg, err := content.NewOCI(tmpStore)
if err != nil {
packagingSpinner.UpdateText(fmt.Sprintf("Failed to initialize temp OCI registry in: %s", tmpStore))
packagingSpinner.Fail()
return err
}
_, err = oras.Copy(ctx, reg, registryRef, tempReg, "",
oras.WithAllowedMediaTypes(spec.AllowedMediaTypes()),
oras.WithPullByBFS)
if err != nil {
packagingSpinner.UpdateText(fmt.Sprintf("Failed to copy image from '%s' to '%s'", opts.general.OCIStorageDir, tmpStore))
packagingSpinner.Fail()
return err
}

dockerfile := tmpDir + "/Dockerfile"
err = os.WriteFile(dockerfile, packagedDockerfile, 0755)
if err != nil {
packagingSpinner.UpdateText(fmt.Sprintf("Failed to write: %s'", dockerfile))
packagingSpinner.Fail()
return err
}

packagedImage := args[1]
err = buildPackagedImage(ctx, opts, registryRef, opts.BeeImage, tmpDir, packagedImage)
if err != nil {
packagingSpinner.UpdateText("Docker build of packaged image failed'")
packagingSpinner.Fail()
return err
}

packagingSpinner.UpdateText(fmt.Sprintf("Packaged image built and tagged at %s", packagedImage))
packagingSpinner.Success()
return nil
}

func buildPackagedImage(
ctx context.Context,
opts *buildOptions,
ociImage, beeImage, tmpDir, packagedImage string,
) error {
dockerArgs := []string{
"build",
"--build-arg",
fmt.Sprintf("BPF_IMAGE=%s", ociImage),
"--build-arg",
fmt.Sprintf("BEE_IMAGE=%s", beeImage),
tmpDir,
"-t",
packagedImage,
}
dockerCmd := exec.CommandContext(ctx, opts.Builder, dockerArgs...)
byt, err := dockerCmd.CombinedOutput()
if err != nil {
fmt.Printf("%s\n", byt)
return err
}
if opts.general.Verbose {
fmt.Printf("%s\n", byt)
}
return nil
}

0 comments on commit 12a3887

Please sign in to comment.