diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3cce48f..69ea45d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -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: diff --git a/Makefile b/Makefile index 2daafa5..0d45df6 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ OUTDIR?=_output HUB?=ghcr.io/solo-io REPO_NAME?=bumblebee EXAMPLES_DIR?=examples - +DOCKER := docker RELEASE := "true" ifeq ($(TAGGED_VERSION),) @@ -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) @@ -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 ##---------------------------------------------------------------------------------- diff --git a/bee/Dockerfile-bee b/bee/Dockerfile-bee new file mode 100644 index 0000000..bf99061 --- /dev/null +++ b/bee/Dockerfile-bee @@ -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 . diff --git a/docs/concepts.md b/docs/concepts.md index f5682e1..de75dc0 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -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 your terminal may be left in a bad state. This is because the 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: diff --git a/pkg/cli/app.go b/pkg/cli/app.go index bb9dc4b..7cef295 100644 --- a/pkg/cli/app.go +++ b/pkg/cli/app.go @@ -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" @@ -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), diff --git a/pkg/cli/internal/commands/build/build.go b/pkg/cli/internal/commands/build/build.go index 2896c8e..cb60851 100644 --- a/pkg/cli/internal/commands/build/build.go +++ b/pkg/cli/internal/commands/build/build.go @@ -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 ") flags.BoolVarP(&opts.Local, "local", "l", false, "Build the output binary and OCI image using local tools") - } func Command(opts *options.GeneralOptions) *cobra.Command { @@ -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 { diff --git a/pkg/cli/internal/commands/package/Dockerfile b/pkg/cli/internal/commands/package/Dockerfile new file mode 100644 index 0000000..0c388e6 --- /dev/null +++ b/pkg/cli/internal/commands/package/Dockerfile @@ -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} diff --git a/pkg/cli/internal/commands/package/package.go b/pkg/cli/internal/commands/package/package.go new file mode 100644 index 0000000..9c8e43e --- /dev/null +++ b/pkg/cli/internal/commands/package/package.go @@ -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 +}