Skip to content

Commit

Permalink
allow bytecode image to ship multiple programs
Browse files Browse the repository at this point in the history
Update the bytecode image backwards compat spec
to allow users to ship multiple programs in a
single image.

Signed-off-by: Andrew Stoycos <[email protected]>
  • Loading branch information
astoycos committed Jun 17, 2024
1 parent e48a86c commit 70e8fb3
Show file tree
Hide file tree
Showing 14 changed files with 1,184 additions and 713 deletions.
731 changes: 370 additions & 361 deletions .github/workflows/image-build.yaml

Large diffs are not rendered by default.

234 changes: 120 additions & 114 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ netlink-packet-core = { version = "^0.7", default-features = false }
netlink-packet-route = { version = "^0.19", default-features = false }
netlink-sys = { version = "^0.8", default-features = false }
nix = { version = "0.28", default-features = false }
object = { version = "0.35.0", default-features = false }
object = { version = "0.32.2", default-features = false }
oci-distribution = { version = "0.10", default-features = false }
opentelemetry = { version = "0.22.0", default-features = false }
opentelemetry-otlp = { version = "0.15.0", default-features = false }
Expand Down Expand Up @@ -89,7 +89,7 @@ tonic-build = { version = "0.11.0", default-features = false }
tower = { version = "0.4.13", default-features = false }
url = { version = "2.5.1", default-features = false }
users = { version = "0.11.0", default-features = false }

aya-obj = { version = "0.1.0", default-features = false }

[workspace.metadata.vendor-filter]
platforms = [
Expand Down
17 changes: 7 additions & 10 deletions Containerfile.bytecode
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
FROM scratch

ARG PROGRAM_NAME
ARG BPF_FUNCTION_NAME
ARG EL_BYTECODE_FILE
ARG EB_BYTECODE_FILE
ARG BYTECODE_FILE
ARG PROGRAMS
ARG MAPS

COPY $EL_BYTECODE_FILE /
COPY $EB_BYTECODE_FILE /
LABEL io.ebpf.filename_eb $EB_BYTECODE_FILE
LABEL io.ebpf.filename_el $EB_BYTECODE_FILE
LABEL io.ebpf.program_name $PROGRAM_NAME
LABEL io.ebpf.bpf_function_name $BPF_FUNCTION_NAME
LABEL "io.ebpf.programs"=$PROGRAMS
LABEL "io.ebpf.maps"=$MAPS

COPY $BYTECODE_FILE /
1 change: 1 addition & 0 deletions bpfman/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ toml = { workspace = true, features = ["parse"] }
tonic = { workspace = true, features = ["transport"] }
tower = { workspace = true }
url = { workspace = true }
aya-obj = { workspace = true }

[dev-dependencies]
assert_matches = { workspace = true }
97 changes: 97 additions & 0 deletions bpfman/src/bin/cli/args.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of bpfman

use std::{io::ErrorKind, path::PathBuf, str::FromStr};

use bpfman::types::ProgramType;
use clap::{Args, Parser, Subcommand};
use hex::FromHex;
Expand Down Expand Up @@ -310,6 +312,101 @@ pub(crate) struct GetArgs {
pub(crate) enum ImageSubCommand {
/// Pull an eBPF bytecode image from a remote registry.
Pull(PullBytecodeArgs),
/// Build an eBPF bytecode image from local bytecode objects.
Build(BuildBytecodeArgs),
/// Generate the OCI image labels for a given bytecode file.
GenerateBuildArgs(GenerateArgs),
}

// Targets understood by bpf2go.
//
// Targets without a Linux string can't be used directly and are only included
// for the generic bpf, bpfel, bpfeb targets.
//
// See https://go.dev/doc/install/source#environment for valid GOARCHes when
// GOOS=linux.
// var targetByGoArch = map[goarch]target{
// "386": {"bpfel", "x86"},
// "amd64": {"bpfel", "x86"},
// "arm": {"bpfel", "arm"},
// "arm64": {"bpfel", "arm64"},
// "loong64": {"bpfel", "loongarch"},
// "mips": {"bpfeb", "mips"},
// "mipsle": {"bpfel", ""},
// "mips64": {"bpfeb", ""},
// "mips64le": {"bpfel", ""},
// "ppc64": {"bpfeb", "powerpc"},
// "ppc64le": {"bpfel", "powerpc"},
// "riscv64": {"bpfel", "riscv"},
// "s390x": {"bpfeb", "s390"},
// }

#[derive(Debug, Clone)]
pub(crate) enum GoArch {
X386,
Amd64,
Arm,
Arm64,
Loong64,
Mips,
Mipsle,
Mips64,
Mips64le,
Ppc64,
Ppc64le,
Riscv64,
S390x,
}

impl FromStr for GoArch {
type Err = std::io::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"386" => Ok(GoArch::X386),
"amd64" => Ok(GoArch::Amd64),
"arm" => Ok(GoArch::Arm),
"arm64" => Ok(GoArch::Arm64),
"loong64" => Ok(GoArch::Loong64),
"mips" => Ok(GoArch::Mips),
"mipsle" => Ok(GoArch::Mipsle),
"mips64" => Ok(GoArch::Mips64),
"mips64le" => Ok(GoArch::Mips64le),
"ppc64" => Ok(GoArch::Ppc64),
"ppc64le" => Ok(GoArch::Ppc64le),
"riscv64" => Ok(GoArch::Riscv64),
"s390x" => Ok(GoArch::S390x),
_ => Err(std::io::Error::new(ErrorKind::InvalidInput, "not a valid bytecode arch, please refer to https://go.dev/doc/install/source#environment for valid GOARCHes when GOOS=linux.")),
}
}
}

#[derive(Args, Debug)]
#[command(disable_version_flag = true)]
pub(crate) struct BuildBytecodeArgs {
/// Required: Dockerfile to use for building the image.
/// Example: --file Containerfile.bytecode
#[clap(short, long, verbatim_doc_comment)]
pub(crate) bytecode_file: PathBuf,

/// Required: Name and optionally a tag in the name:tag format.
/// Example: --tag quay.io/bpfman-bytecode/xdp_pass:latest
#[clap(short, long, verbatim_doc_comment)]
pub(crate) tag: String,

/// Required: Dockerfile to use for building the image.
/// Example: --file Containerfile.bytecode
#[clap(short = 'f', long, verbatim_doc_comment)]
pub(crate) container_file: PathBuf,
}

#[derive(Args, Debug)]
#[command(disable_version_flag = true)]
pub(crate) struct GenerateArgs {
/// Required: Dockerfile to use for building the image.
/// Example: --file Containerfile.bytecode
#[clap(short, long, verbatim_doc_comment)]
pub(crate) bytecode_file: PathBuf,
}

#[derive(Args, Debug)]
Expand Down
178 changes: 176 additions & 2 deletions bpfman/src/bin/cli/image.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of bpfman

use anyhow::{anyhow, Context, Result};
use aya_obj::Object;
use base64::{engine::general_purpose, Engine};
use bpfman::{
pull_bytecode,
types::{BytecodeImage, ImagePullPolicy},
types::{BytecodeImage, ImagePullPolicy, MapType, ProgramType},
};
use log::debug;
use object::Endianness;
use std::{collections::HashMap, env, fs, path::Path, process::Command};

use crate::args::{ImageSubCommand, PullBytecodeArgs};
use crate::args::{BuildBytecodeArgs, GenerateArgs, ImageSubCommand, PullBytecodeArgs};

impl ImageSubCommand {
pub(crate) async fn execute(&self) -> anyhow::Result<()> {
match self {
ImageSubCommand::Pull(args) => execute_pull(args).await,
ImageSubCommand::Build(args) => execute_build(args).await,
ImageSubCommand::GenerateBuildArgs(args) => execute_build_args(args).await,
}
}
}
Expand Down Expand Up @@ -47,3 +54,170 @@ pub(crate) async fn execute_pull(args: &PullBytecodeArgs) -> anyhow::Result<()>

Ok(())
}

pub(crate) async fn execute_build(args: &BuildBytecodeArgs) -> anyhow::Result<()> {
// parse program data from bytecode file
let (prog_labels, map_labels) =
build_image_labels(&args.bytecode_file, Some(Endianness::default()))?;
debug!(
"Bytecode: {} contains the following. \n
programs: {prog_labels}\n
maps: {map_labels}",
args.bytecode_file.display()
);
let container_tool = ContainerRuntime::new()?;
container_tool.build_image(
&args.tag,
&args.bytecode_file,
&args.container_file,
prog_labels,
map_labels,
)?;

Ok(())
}

pub(crate) async fn execute_build_args(args: &GenerateArgs) -> anyhow::Result<()> {
// parse program data from bytecode file
let (prog_labels, map_labels) =
build_image_labels(&args.bytecode_file, Some(Endianness::default()))?;
debug!(
"Bytecode: {} contains the following. \n
programs: {prog_labels}\n
maps: {map_labels}",
args.bytecode_file.display()
);

println!("PROGRAMS={}", prog_labels);
println!("MAPS={}", map_labels);

Ok(())
}

enum ContainerRuntime {
Docker,
Podman,
}

impl ContainerRuntime {
// Default to using docker if it's available.
fn new() -> Result<Self, anyhow::Error> {
if Command::new("docker")
.arg("version")
.output()
.is_ok_and(|o| o.status.success())
{
debug!("using docker for container runtime");
Ok(ContainerRuntime::Docker)
} else if Command::new("podman")
.arg("version")
.output()
.is_ok_and(|o| o.status.success())
{
debug!("using podman for container runtime");
Ok(ContainerRuntime::Podman)
} else {
Err(anyhow!(
"No container runtime found. Please install either docker or podman."
))
}
}

fn command(&self) -> Command {
match self {
ContainerRuntime::Docker => Command::new("docker"),
ContainerRuntime::Podman => Command::new("podman"),
}
}

fn build_image(
&self,
image_tag: &str,
bc_file: &Path,
container_file: &Path,
program_labels: String,
map_labels: String,
) -> anyhow::Result<()> {
let mut command = self.command();
command.arg("build");
command.arg("-t").arg(image_tag);
command.arg("-f").arg(container_file);

command
.arg("--build-arg")
.arg(format! {"PROGRAMS={}",program_labels});
command
.arg("--build-arg")
.arg(format! {"MAPS={}",map_labels});

let current_dir = env::current_dir()?;

let build_arg = format!("BYTECODE_FILE={}", bc_file.display());
command.arg("--build-arg").arg(build_arg);
command.arg(current_dir.as_os_str().to_str().unwrap_or("."));

debug!(
"Building bytecode images with command: {} {}",
command.get_program().to_string_lossy(),
command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect::<Vec<String>>()
.join(" ")
);

let output = command.output()?;
if !output.status.success() {
return Err(anyhow!(
"Failed to build image: {}",
String::from_utf8(output.stderr)?
));
}

Ok(())
}
}

fn build_image_labels(
file: &Path,
expected_endianness: Option<object::Endianness>,
) -> Result<(String, String), anyhow::Error> {
let bc_content = fs::read(file).context("cannot find bytecode")?;
let bc = Object::parse(&bc_content)?;

if expected_endianness.is_some_and(|e| e != bc.endianness) {
return Err(anyhow!(
"Bytcode: {file:?} doesn't match expected endianness {expected_endianness:?}"
));
}

if bc.programs.is_empty() {
return Err(anyhow!("No programs found in bytecode: {file:?}"));
}

let program_labels: HashMap<String, String> = bc
.programs
.into_iter()
.map(|(k, v)| {
let prog_type = ProgramType::from(v.section);
(k, prog_type.to_string())
})
.collect();

let map_labels: HashMap<String, String> = bc
.maps
.into_iter()
.filter_map(|(name, v)| {
if !(name.contains(".rodata") || name.contains(".bss") || name.contains(".data")) {
let map_type = MapType::from(v.map_type());
Some((name, map_type.to_string()))
} else {
None
}
})
.collect();
Ok((
serde_json::to_string(&program_labels)?,
serde_json::to_string(&map_labels)?,
))
}
8 changes: 8 additions & 0 deletions bpfman/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ pub enum BpfmanError {
image_prog_name: String,
provided_prog_name: String,
},
#[error("Program {expected_prog_name} not found in bytecode image {bytecode_image} with program names {program_names:?}")]
ProgramNotFoundInBytecode {
bytecode_image: String,
expected_prog_name: String,
program_names: Vec<String>,
},
#[error("Unable to delete program {0}")]
BpfmanProgramDeleteError(#[source] anyhow::Error),
#[error(transparent)]
Expand Down Expand Up @@ -69,6 +75,8 @@ pub enum BpfmanError {
pub enum ParseError {
#[error("{program} is not a valid program type")]
InvalidProgramType { program: String },
#[error("{map} is not a valid map type")]
InvalidMapType { map: String },
#[error("{proceedon} is not a valid proceed-on value")]
InvalidProceedOn { proceedon: String },
#[error("not a valid direction: {direction}")]
Expand Down
Loading

0 comments on commit 70e8fb3

Please sign in to comment.