Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Include list of image layer directories in ContainerInfo #294

Merged
merged 9 commits into from
Sep 18, 2024
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/bpf-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ procfs = { workspace = true }
libc = { workspace = true }
glob = { workspace = true }
hex = { workspace = true }
hyper = { workspace = true }
hyperlocal = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
lazy_static = { workspace = true }
Expand Down
193 changes: 193 additions & 0 deletions crates/bpf-common/src/containers/layers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use std::{
fmt,
fs::{self, File},
io::BufReader,
path::PathBuf,
str::FromStr,
};

use hyper::{body, Client};
use hyperlocal::{UnixClientExt, Uri as HyperlocalUri};
use serde::Deserialize;

use super::ContainerError;

const DOCKER_SOCKET: &str = "/var/run/docker.sock";

/// Docker API response for `image inspect` request.
#[derive(Debug, Deserialize)]
struct ImageInspect {
#[serde(rename = "GraphDriver")]
graph_driver: GraphDriver,
}

#[derive(Debug, Deserialize)]
struct GraphDriver {
#[serde(rename = "Data")]
data: Option<GraphDriverData>,
#[serde(rename = "Name")]
name: GraphDriverName,
}

#[derive(Debug, Deserialize)]
struct GraphDriverData {
#[serde(rename = "LowerDir")]
lower_dir: Option<String>,
#[serde(rename = "MergedDir")]
merged_dir: Option<PathBuf>,
#[serde(rename = "UpperDir")]
upper_dir: Option<PathBuf>,
#[serde(rename = "WorkDir")]
work_dir: Option<PathBuf>,
}

#[derive(Debug, Deserialize)]
enum GraphDriverName {
#[serde(rename = "btrfs")]
Btrfs,
#[serde(rename = "fuse-overlayfs")]
FuseOverlayfs,
#[serde(rename = "overlay2")]
Overlayfs,
#[serde(rename = "vfs")]
Vfs,
#[serde(rename = "zfs")]
Zfs,
}

impl fmt::Display for GraphDriverName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Btrfs => write!(f, "btrfs"),
Self::FuseOverlayfs => write!(f, "fuse-overlayfs"),
Self::Overlayfs => write!(f, "overlay2"),
Self::Vfs => write!(f, "vfs"),
Self::Zfs => write!(f, "zfs"),
}
}
}

#[derive(Debug, Deserialize)]
struct ImageDbEntry {
rootfs: Rootfs,
}

#[derive(Debug, Deserialize)]
struct Rootfs {
diff_ids: Vec<String>,
}

/// Returns a list of layer paths for the given Docker image ID.
pub(crate) async fn docker_layers(image_id: &str) -> Result<Vec<PathBuf>, ContainerError> {
let client = Client::unix();
let uri = HyperlocalUri::new(DOCKER_SOCKET, &format!("/images/{}/json", image_id));
let uri: hyper::Uri = uri.into();

let response =
client
.get(uri.clone())
.await
.map_err(|source| ContainerError::HyperRequest {
source,
uri: uri.clone(),
})?;
let body_bytes =
body::to_bytes(response)
.await
.map_err(|source| ContainerError::HyperResponse {
source,
uri: uri.clone(),
})?;

let response: ImageInspect = serde_json::from_slice(&body_bytes)
.map_err(|source| ContainerError::ParseResponse { source, uri })?;

match response.graph_driver.name {
GraphDriverName::Btrfs => docker_btrfs_layers(image_id),
GraphDriverName::Overlayfs => docker_overlayfs_layers(response.graph_driver.data),
_ => {
log::warn!(
"Docker graph driver {} is unsupported",
response.graph_driver.name
);
Ok(Vec::new())
}
}
}

/// Returns a list of BTRFS layer paths for the given Docker image ID.
///
/// The procedure for BTRFS is not straigthforward, since the `image inspect`
/// response doesn't have direct information about layer directories. It
/// consists of the following steps:
///
/// 1. Using the given image ID, find an "imagedb entry". It's located in
/// `/var/lib/docker/image/btrfs/imagedb/content/sha256/<image_id>`.
/// 2. Get the list of layer checksums from that entry.
/// 3. For each layer, check whether a "layerdb entry" exists. It's located
/// in `/var/lib/docker/image/btrfs/layerdb/sha256/<layer_id>`. The
/// layerdb directory contains a `cache-id` file.
/// 4. That `cache-id` file contains an ID of a BTRFS subvolume. The
/// subvolume can be found in `/var/lib/docker/btrfs/subvolumes/<cache_id>`.
fn docker_btrfs_layers(image_id: &str) -> Result<Vec<PathBuf>, ContainerError> {
const DOCKER_IMAGEDB_PATH: &str = "/var/lib/docker/image/btrfs/imagedb/content/sha256/";
const DOCKER_LAYERDB_PATH: &str = "/var/lib/docker/image/btrfs/layerdb/sha256/";
const DOCKER_BTRFS_SUBVOL_PATH: &str = "/var/lib/docker/btrfs/subvolumes/";

let mut layers = Vec::new();

let path = PathBuf::from(DOCKER_IMAGEDB_PATH).join(image_id);
let file = File::open(&path).map_err(|source| ContainerError::ReadFile {
source,
path: path.clone(),
})?;

let reader = BufReader::new(file);
let imagedb_entry: ImageDbEntry = serde_json::from_reader(reader)
.map_err(|source| ContainerError::ParseConfigFile { source, path })?;

for layer_id in imagedb_entry.rootfs.diff_ids {
let layer_id = layer_id
.split(':')
.last()
.ok_or(ContainerError::InvalidLayerID(layer_id.clone()))?;

let path = PathBuf::from(DOCKER_LAYERDB_PATH).join(layer_id);
if path.exists() {
let path = path.join("cache-id");
let btrfs_subvol_id = fs::read_to_string(&path)
.map_err(|source| ContainerError::ReadFile { source, path })?;
let btrfs_subvol_path = PathBuf::from(DOCKER_BTRFS_SUBVOL_PATH).join(btrfs_subvol_id);

layers.push(btrfs_subvol_path);
}
}

Ok(layers)
}

fn docker_overlayfs_layers(
graph_driver_data: Option<GraphDriverData>,
) -> Result<Vec<PathBuf>, ContainerError> {
let mut layers = Vec::new();

if let Some(graph_driver_data) = graph_driver_data {
if let Some(lower_dirs) = graph_driver_data.lower_dir {
for lower_dir in lower_dirs.split(':') {
// `PathBuf::from_str` is infallible.
layers.push(PathBuf::from_str(lower_dir).unwrap());
}
}
if let Some(merged_dir) = graph_driver_data.merged_dir {
layers.push(merged_dir);
}
if let Some(upper_dir) = graph_driver_data.upper_dir {
layers.push(upper_dir);
}
if let Some(work_dir) = graph_driver_data.work_dir {
layers.push(work_dir);
}
}

Ok(layers)
}
49 changes: 46 additions & 3 deletions crates/bpf-common/src/containers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use validatron::Validatron;

use crate::parsing::procfs::ProcfsError;

pub mod layers;
pub mod schema;

#[derive(Error, Debug)]
Expand All @@ -40,8 +41,26 @@ pub enum ContainerError {
source: serde_json::error::Error,
path: PathBuf,
},
#[error("parsing response from `{uri:?}` failed")]
ParseResponse {
#[source]
source: serde_json::error::Error,
uri: hyper::Uri,
},
#[error("path `{path}` is non-UTF-8")]
PathNonUtf8 { path: PathBuf },
#[error("failed to make a request to the UNIX socket `{uri:?}`")]
HyperRequest {
#[source]
source: hyper::Error,
uri: hyper::Uri,
},
#[error("failed to parse a response from the UNIX socket `{uri:?}`")]
HyperResponse {
#[source]
source: hyper::Error,
uri: hyper::Uri,
},
#[error("could not connect to the database `{path:?}`")]
SqliteConnection {
#[source]
Expand Down Expand Up @@ -78,6 +97,10 @@ pub enum ContainerError {
BoltBucketNotFound(String),
#[error("bolt key `{0}` not found")]
BoltKeyNotFound(String),
#[error("Invalid layer ID: `{0}`")]
InvalidLayerID(String),
#[error("Invalid image digest: `{0}`")]
InvalidImageDigest(String),
}

/// A container ID.
Expand Down Expand Up @@ -145,6 +168,8 @@ pub struct ContainerInfo {
pub name: String,
pub image: String,
pub image_digest: String,
#[validatron(skip)]
pub layers: Vec<PathBuf>,
}

impl fmt::Display for ContainerInfo {
Expand All @@ -158,19 +183,19 @@ impl fmt::Display for ContainerInfo {
}

impl ContainerInfo {
pub fn from_container_id(
pub async fn from_container_id(
container_id: ContainerId,
uid: Uid,
) -> Result<Option<Self>, ContainerError> {
let info = match container_id {
ContainerId::Docker(id) => Self::from_docker_id(id),
ContainerId::Docker(id) => Self::from_docker_id(id).await,
ContainerId::Libpod(id) => Self::from_libpod_id(id, uid),
};

info.map(Some)
}

fn from_docker_id(id: String) -> Result<Self, ContainerError> {
async fn from_docker_id(id: String) -> Result<Self, ContainerError> {
const DOCKER_CONTAINERS_PATH: &str = "/var/lib/docker/containers";

let path = PathBuf::from(DOCKER_CONTAINERS_PATH)
Expand All @@ -194,11 +219,27 @@ impl ContainerInfo {
let image = config.config.image;
let image_digest = config.image_digest;

// `image_digest` has format like:
//
// ```
// sha256:1d34ffeaf190be23d3de5a8de0a436676b758f48f835c3a2d4768b798c15a7f1
// ```
//
// The unprefixed digest is used as an image ID.
let image_id = image_digest
.split(':')
.last()
.ok_or(ContainerError::InvalidImageDigest(image_digest.clone()))?;

let layers = layers::docker_layers(image_id).await?;
log::debug!("found layer filesystems for container {id}: {layers:?}");

Ok(Self {
id,
name,
image,
image_digest,
layers,
})
}

Expand Down Expand Up @@ -266,6 +307,8 @@ impl ContainerInfo {
name: config.name,
image: config.rootfs_image_name,
image_digest: image.digest.clone(),
// TODO(vadorovsky): Parse layer information in Podman.
banditopazzo marked this conversation as resolved.
Show resolved Hide resolved
layers: Vec::new(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happened to the previous changes of #309 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must've done a wrong git push -f. I restored them now, sorry.

})
}
}
Expand Down
Loading