Skip to content

Commit

Permalink
feat: support docker language
Browse files Browse the repository at this point in the history
  • Loading branch information
bxb100 committed Nov 15, 2024
1 parent 2aec6f0 commit b92fe97
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 3 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ repos:
rev: v1.26.0
hooks:
- id: typos
- id: typos-docker

- repo: local
hooks:
Expand Down
19 changes: 17 additions & 2 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ home = "0.5.9"
indicatif = "0.17.8"
indoc = "2.0.5"
itertools = "0.13.0"
md5 = "0.7.0"
owo-colors = "4.1.0"
rand = "0.8.5"
rayon = "1.10.0"
regex-lite = "0.1.6"
rusqlite = { version = "0.32.1", features = ["bundled"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_yaml = "0.9.34"
Expand All @@ -39,6 +41,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
unicode-width = "0.2.0"
url = { version = "2.5.2", features = ["serde"] }
which = "6.0.3"
serde_json = "1.0.132"

[dev-dependencies]
assert_fs = "1.1.2"
Expand Down
227 changes: 227 additions & 0 deletions src/languages/docker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
use crate::config::Language;
use crate::fs::CWD;
use crate::hook::Hook;
use crate::languages::{LanguageImpl, DEFAULT_VERSION};
use crate::run::run_by_batch;
use assert_cmd::output::{OutputError, OutputOkExt};
use regex_lite::Regex;
use serde_json::Value;
use std::borrow::Cow;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::panic::catch_unwind;
use std::path::Path;
use std::sync::Arc;
use tokio::process::Command;
use tracing::debug;

const PRE_COMMIT_LABEL: &str = "PRE_COMMIT";

#[derive(Debug, Copy, Clone)]
pub struct Docker;

impl Docker {
fn docker_tag(hook: &Hook) -> Option<String> {
hook.path()
.file_name()
.and_then(OsStr::to_str)
.map(|s| format!("pre-commit-{:x}", md5::compute(s)))
}

async fn build_docker_image(hook: &Hook, pull: bool) -> anyhow::Result<()> {
let mut cmd = Command::new("docker");

let cmd = cmd.arg("build").args([
"--tag",
&Self::docker_tag(hook).expect("Tag can't generate"),
"--label",
PRE_COMMIT_LABEL,
]);

if pull {
cmd.arg("--pull");
}

// This must come last for old versions of docker.
// see https://github.com/pre-commit/pre-commit/issues/477
cmd.arg(".");

debug!(cmd=?cmd, "docker build_docker_image:");

cmd.current_dir(hook.path())
.output()
.await
.map_err(OutputError::with_cause)?
.ok()?;

Ok(())
}

fn is_in_docker() -> bool {
match fs_err::read_to_string("/proc/self/mountinfo") {
Ok(mounts) => mounts.contains("docker"),
Err(_) => false,
}
}

/// It should check [`Self::is_in_docker`] first
///
/// there are no valid algorithm to get container id inner container, see <https://stackoverflow.com/questions/20995351/how-can-i-get-docker-linux-container-information-from-within-the-container-itsel>
fn get_container_id() -> String {
// https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/7167/files#diff-9c4c3c88c89f52b9a08884bf4e1389271c7d8c32a977438b5dac460a8edabc7b
let regex = Regex::new(r".*/docker/containers/([0-9a-f]{64})/.*").unwrap();
let v2_group_path = fs_err::read_to_string("/proc/self/mountinfo")
.expect("Failed to find the container ID");

let (_, [id]) = regex.captures(&v2_group_path).unwrap().extract();

id.to_string()
}

async fn get_docker_path(path: &Path) -> Cow<'_, str> {
if !Self::is_in_docker() {
return path.to_string_lossy();
};

let container_id = Self::get_container_id();
if let Ok(output) = Command::new("docker")
.args(["inspect", "--format", "'{{json .Mounts}}'", &container_id])
.output()
.await
{
if let Ok(Value::Array(array)) = serde_json::from_slice(&output.stdout) {
for item in array {
if let Value::Object(map) = item {
let src_path = map.get("Source").unwrap().as_str().unwrap();
let to_path = map.get("Destination").unwrap().as_str().unwrap();
if path.starts_with(to_path) {
return Cow::from(path.to_string_lossy().replace(to_path, src_path));
}
}
}
}
}

path.to_string_lossy()
}

/// see <https://stackoverflow.com/questions/57951893/how-to-determine-the-effective-user-id-of-a-process-in-rust>
fn get_docker_user() -> Option<[String; 2]> {
catch_unwind(|| unsafe { ["-u".to_owned(), format!("{}:{}", geteuid(), getegid())] }).ok()
}

fn get_docker_tty(color: bool) -> Option<String> {
if color {
Some("--tty".to_owned())
} else {
None
}
}

async fn docker_cmd(color: bool) -> Command {
let mut command = Command::new("docker");
command.args(["run", "--rm"]);
if let Some(tty) = Self::get_docker_tty(color) {
command.arg(&tty);
}
if let Some(user) = Self::get_docker_user() {
command.args(user);
}
command.args([
"-v",
// https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from
&format!("{}:/src:rw,Z", Self::get_docker_path(&CWD).await),
"--workdir",
"/src",
]);

command
}
}

#[link(name = "c")]
extern "C" {
fn geteuid() -> u32;
fn getegid() -> u32;
}

impl LanguageImpl for Docker {
fn name(&self) -> Language {
Language::Docker
}

fn default_version(&self) -> &str {
DEFAULT_VERSION
}

fn environment_dir(&self) -> Option<&str> {
Some("docker")
}

async fn install(&self, hook: &Hook) -> anyhow::Result<()> {
let env = hook.environment_dir().expect("No environment dir found");
debug!(path=?hook.path(), env=?env, "docker install:");
Docker::build_docker_image(hook, true).await?;
fs_err::create_dir_all(env)?;
Ok(())
}

async fn check_health(&self) -> anyhow::Result<()> {
todo!()
}

async fn run(
&self,
hook: &Hook,
filenames: &[&String],
env_vars: Arc<HashMap<&'static str, String>>,
) -> anyhow::Result<(i32, Vec<u8>)> {
Docker::build_docker_image(hook, false).await?;

let docker_tag = Docker::docker_tag(hook).unwrap();

let cmds = shlex::split(&hook.entry).ok_or(anyhow::anyhow!("Failed to parse entry"))?;

let cmds = Arc::new(cmds);
let hook_args = Arc::new(hook.args.clone());

let run = move |batch: Vec<String>| {
let cmds = cmds.clone();
let docker_tag = docker_tag.clone();
let hook_args = hook_args.clone();
let env_vars = env_vars.clone();

async move {
// docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
let mut cmd = Docker::docker_cmd(true).await;
let cmd = cmd
.args(["--entrypoint", &cmds[0], &docker_tag])
.args(&cmds[1..])
.args(hook_args.as_ref())
.args(batch)
.stderr(std::process::Stdio::inherit())
.envs(env_vars.as_ref());

debug!(cmd=?cmd, "Docker run batch:");

let mut output = cmd.output().await?;
output.stdout.extend(output.stderr);
let code = output.status.code().unwrap_or(1);
anyhow::Ok((code, output.stdout))
}
};

let results = run_by_batch(hook, filenames, run).await?;

// Collect results
let mut combined_status = 0;
let mut combined_output = Vec::new();

for (code, output) in results {
combined_status |= code;
combined_output.extend(output);
}

Ok((combined_status, combined_output))
}
}
11 changes: 10 additions & 1 deletion src/languages/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use anyhow::Result;
use crate::config;
use crate::hook::Hook;

mod docker;
mod fail;
mod node;
mod python;
Expand Down Expand Up @@ -34,6 +35,7 @@ pub enum Language {
Node(node::Node),
System(system::System),
Fail(fail::Fail),
Docker(docker::Docker),
}

impl From<config::Language> for Language {
Expand All @@ -42,7 +44,7 @@ impl From<config::Language> for Language {
// config::Language::Conda => Language::Conda,
// config::Language::Coursier => Language::Coursier,
// config::Language::Dart => Language::Dart,
// config::Language::Docker => Language::Docker,
config::Language::Docker => Language::Docker(docker::Docker),
// config::Language::DockerImage => Language::DockerImage,
// config::Language::Dotnet => Language::Dotnet,
config::Language::Fail => Language::Fail(fail::Fail),
Expand Down Expand Up @@ -71,6 +73,7 @@ impl Display for Language {
Self::Node(node) => node.fmt(f),
Self::System(system) => system.fmt(f),
Self::Fail(fail) => fail.fmt(f),
Self::Docker(docker) => docker.fmt(f),
}
}
}
Expand All @@ -82,6 +85,7 @@ impl Language {
Self::Node(node) => node.name(),
Self::System(system) => system.name(),
Self::Fail(fail) => fail.name(),
Self::Docker(docker) => docker.name(),
}
}

Expand All @@ -91,6 +95,7 @@ impl Language {
Self::Node(node) => node.default_version(),
Self::System(system) => system.default_version(),
Self::Fail(fail) => fail.default_version(),
Self::Docker(docker) => docker.default_version(),
}
}

Expand All @@ -100,6 +105,7 @@ impl Language {
Self::Node(node) => node.environment_dir(),
Self::System(system) => system.environment_dir(),
Self::Fail(fail) => fail.environment_dir(),
Self::Docker(docker) => docker.environment_dir(),
}
}

Expand All @@ -109,6 +115,7 @@ impl Language {
Self::Node(node) => node.install(hook).await,
Self::System(system) => system.install(hook).await,
Self::Fail(fail) => fail.install(hook).await,
Self::Docker(docker) => docker.install(hook).await,
}
}

Expand All @@ -118,6 +125,7 @@ impl Language {
Self::Node(node) => node.check_health().await,
Self::System(system) => system.check_health().await,
Self::Fail(fail) => fail.check_health().await,
Self::Docker(docker) => docker.check_health().await,
}
}

Expand All @@ -132,6 +140,7 @@ impl Language {
Self::Node(node) => node.run(hook, filenames, env_vars).await,
Self::System(system) => system.run(hook, filenames, env_vars).await,
Self::Fail(fail) => fail.run(hook, filenames, env_vars).await,
Self::Docker(docker) => docker.run(hook, filenames, env_vars).await,
}
}
}

0 comments on commit b92fe97

Please sign in to comment.