@@ -1,20 +1,20 @@
|
||||
//! Container lifecycle primitives for a parsed [`DevContainer`].
|
||||
//! Primitives de cycle de vie de container pour un [`DevContainer`] analysé.
|
||||
//!
|
||||
//! This module shells out to a container runtime (`docker` or `podman`) to build
|
||||
//! the devcontainer image, start a container with the workspace mounted, run the
|
||||
//! `postCreateCommand` / `postStartCommand` hooks and execute commands inside the
|
||||
//! running container.
|
||||
//! Ce module invoque un runtime de containers (`docker` ou `podman`) pour construire
|
||||
//! l'image devcontainer, démarrer un container avec le workspace monté, exécuter les
|
||||
//! hooks `postCreateCommand` / `postStartCommand` et lancer des commandes à l'intérieur
|
||||
//! du container en cours d'exécution.
|
||||
//!
|
||||
//! It is intentionally runtime-agnostic: any binary exposing the `docker` CLI
|
||||
//! surface (including `podman`) can be used via [`ContainerRuntime::new`].
|
||||
//! Il est volontairement agnostique du runtime : tout binaire exposant l'interface
|
||||
//! CLI `docker` (y compris `podman`) peut être utilisé via [`ContainerRuntime::new`].
|
||||
//!
|
||||
//! # Isolation
|
||||
//!
|
||||
//! Each sandbox gets its own image tag, its own container and its own network.
|
||||
//! The container starts attached to that network so the `postCreateCommand` /
|
||||
//! `postStartCommand` hooks can fetch dependencies (e.g. `npm install`); once the
|
||||
//! hooks have run, the container is disconnected from the network for the rest of
|
||||
//! its lifetime. Every command is bounded by a timeout.
|
||||
//! Chaque sandbox dispose de son propre tag d'image, de son propre container et de
|
||||
//! son propre réseau. Le container démarre attaché à ce réseau afin que les hooks
|
||||
//! `postCreateCommand` / `postStartCommand` puissent récupérer des dépendances
|
||||
//! (par ex. `npm install`) ; une fois les hooks exécutés, le container est déconnecté
|
||||
//! du réseau pour le reste de sa durée de vie. Chaque commande est bornée par un timeout.
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
@@ -26,16 +26,16 @@ use tokio::process::Command;
|
||||
|
||||
use crate::DevContainer;
|
||||
|
||||
/// Timeout applied to build/run/stop/remove operations.
|
||||
/// Timeout appliqué aux opérations de build/run/stop/remove.
|
||||
const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(600);
|
||||
|
||||
/// Timeout applied to commands executed inside a running container.
|
||||
/// Timeout appliqué aux commandes exécutées dans un container en cours d'exécution.
|
||||
const DEFAULT_EXEC_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Result of a command executed inside a container.
|
||||
/// Résultat d'une commande exécutée dans un container.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecOutput {
|
||||
/// Exit code, or `-1` if the process was terminated by a signal.
|
||||
/// Code de sortie, ou `-1` si le processus a été terminé par un signal.
|
||||
pub status: i32,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
@@ -46,7 +46,7 @@ impl ExecOutput {
|
||||
self.status == 0
|
||||
}
|
||||
|
||||
/// Turns a non-zero exit code into a [`ContainerError::Command`].
|
||||
/// Transforme un code de sortie non nul en [`ContainerError::Command`].
|
||||
pub fn ensure_success(self, program: &str, args: &[String]) -> Result<Self, ContainerError> {
|
||||
if self.success() {
|
||||
return Ok(self);
|
||||
@@ -85,7 +85,7 @@ pub enum ContainerError {
|
||||
},
|
||||
}
|
||||
|
||||
/// A container runtime binary exposing the `docker` CLI surface.
|
||||
/// Un binaire de runtime de containers exposant l'interface CLI `docker`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContainerRuntime {
|
||||
program: String,
|
||||
@@ -108,7 +108,7 @@ impl ContainerRuntime {
|
||||
Self::new("podman")
|
||||
}
|
||||
|
||||
/// Overrides the timeout applied to build/run/stop/remove operations.
|
||||
/// Remplace le timeout appliqué aux opérations de build/run/stop/remove.
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
@@ -122,7 +122,7 @@ impl ContainerRuntime {
|
||||
self.timeout
|
||||
}
|
||||
|
||||
/// Checks that the runtime binary is present and responsive.
|
||||
/// Vérifie que le binaire du runtime est présent et répond.
|
||||
pub async fn available(&self) -> bool {
|
||||
Command::new(&self.program)
|
||||
.arg("version")
|
||||
@@ -135,15 +135,15 @@ impl ContainerRuntime {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Runs the runtime with the given arguments, capturing stdout/stderr.
|
||||
/// Exécute le runtime avec les arguments donnés, en capturant stdout/stderr.
|
||||
///
|
||||
/// Only spawn failures and timeouts are reported as errors; a non-zero exit
|
||||
/// code is returned in the [`ExecOutput`] so callers can decide how to react.
|
||||
/// Seuls les échecs de lancement (spawn) et les timeouts sont des erreurs ; un
|
||||
/// code non nul est renvoyé dans [`ExecOutput`] afin que les appelants réagissent.
|
||||
pub async fn run(&self, args: &[String]) -> Result<ExecOutput, ContainerError> {
|
||||
self.run_with_timeout(args, self.timeout).await
|
||||
}
|
||||
|
||||
/// Like [`run`](Self::run) with an explicit timeout.
|
||||
/// Comme [`run`](Self::run), mais avec un timeout explicite.
|
||||
pub async fn run_with_timeout(
|
||||
&self,
|
||||
args: &[String],
|
||||
@@ -174,7 +174,7 @@ impl ContainerRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// A running devcontainer.
|
||||
/// Un devcontainer en cours d'exécution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Container {
|
||||
runtime: ContainerRuntime,
|
||||
@@ -194,10 +194,10 @@ impl Container {
|
||||
&self.workspace_folder
|
||||
}
|
||||
|
||||
/// Executes a command inside the container, returning its output.
|
||||
/// Exécute une commande dans le container et renvoie sa sortie.
|
||||
///
|
||||
/// The command is passed as an argv vector (no shell), so no quoting or
|
||||
/// interpolation is performed.
|
||||
/// La commande est transmise sous forme de vecteur d'arguments (argv, sans shell),
|
||||
/// donc aucun échappement ni interpolation n'est effectué.
|
||||
pub async fn exec(&self, cmd: &[&str]) -> Result<ExecOutput, ContainerError> {
|
||||
self.exec_with_timeout(cmd, DEFAULT_EXEC_TIMEOUT).await
|
||||
}
|
||||
@@ -220,12 +220,12 @@ impl Container {
|
||||
self.runtime.run_with_timeout(&args, timeout).await
|
||||
}
|
||||
|
||||
/// Executes a shell script inside the container via `sh -c`.
|
||||
/// Exécute un script shell dans le container via `sh -c`.
|
||||
pub async fn exec_shell(&self, script: &str) -> Result<ExecOutput, ContainerError> {
|
||||
self.exec(&["sh", "-c", script]).await
|
||||
}
|
||||
|
||||
/// Stops the container.
|
||||
/// Arrête le container.
|
||||
pub async fn stop(&self) -> Result<(), ContainerError> {
|
||||
let args = vec!["stop".to_string(), self.name.clone()];
|
||||
self.runtime
|
||||
@@ -235,9 +235,9 @@ impl Container {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the container, its network and its image.
|
||||
/// Supprime le container, son réseau et son image.
|
||||
///
|
||||
/// Network and image removal are best-effort: they may already be gone.
|
||||
/// La suppression du réseau et de l'image est best-effort : ils peuvent déjà être absents.
|
||||
pub async fn remove(&self) -> Result<(), ContainerError> {
|
||||
let args = vec![
|
||||
"rm".to_string(),
|
||||
@@ -265,21 +265,21 @@ impl Container {
|
||||
}
|
||||
|
||||
impl DevContainer {
|
||||
/// Base image name derived from the devcontainer name.
|
||||
/// Nom de l'image de base dérivé du nom du devcontainer.
|
||||
pub fn image_name(&self) -> String {
|
||||
let base = self.name.as_deref().unwrap_or("devcontainer");
|
||||
format!("devcontainer-rs/{}", sanitize(base))
|
||||
}
|
||||
|
||||
/// Unique image tag for a single sandbox run.
|
||||
/// Tag d'image unique pour une exécution de sandbox donnée.
|
||||
///
|
||||
/// Uniqueness matters: two concurrent sandboxes (possibly for different
|
||||
/// repositories sharing a devcontainer name) must not race on a shared tag.
|
||||
/// L'unicité est importante : deux sandboxes concurrentes (éventuellement pour des
|
||||
/// dépôts différents partageant un nom de devcontainer) ne doivent pas se disputer le même tag.
|
||||
pub fn image_tag(&self) -> String {
|
||||
format!("{}:{}", self.image_name(), unique_suffix())
|
||||
}
|
||||
|
||||
/// Arguments passed to `docker build` (everything after the `build` verb).
|
||||
/// Arguments passés à `docker build` (tout ce qui suit le verbe `build`).
|
||||
pub fn build_args(&self, image_tag: &str) -> Vec<String> {
|
||||
let context = self
|
||||
.container_file_path
|
||||
@@ -302,7 +302,7 @@ impl DevContainer {
|
||||
args
|
||||
}
|
||||
|
||||
/// Arguments passed to `docker run` (everything after the `run` verb).
|
||||
/// Arguments passés à `docker run` (tout ce qui suit le verbe `run`).
|
||||
pub fn run_args(
|
||||
&self,
|
||||
workspace_dir: &Path,
|
||||
@@ -340,27 +340,27 @@ impl DevContainer {
|
||||
args.extend(self.run_args.iter().cloned());
|
||||
|
||||
args.push(image_tag.to_string());
|
||||
// Keep the container alive so we can `exec` into it.
|
||||
// Maintient le container en vie pour pouvoir y exécuter `exec`.
|
||||
args.push("sleep".to_string());
|
||||
args.push("infinity".to_string());
|
||||
|
||||
args
|
||||
}
|
||||
|
||||
/// Workspace folder inside the container, defaulting to `/workspaces/workspace`.
|
||||
/// Dossier de workspace dans le container, par défaut `/workspaces/workspace`.
|
||||
pub fn workspace_folder(&self) -> String {
|
||||
self.workspace_folder
|
||||
.clone()
|
||||
.unwrap_or_else(|| "/workspaces/workspace".to_string())
|
||||
}
|
||||
|
||||
/// Unique container name for this run.
|
||||
/// Nom de container unique pour cette exécution.
|
||||
pub fn container_name(&self) -> String {
|
||||
let base = self.name.as_deref().unwrap_or("devcontainer");
|
||||
format!("devcontainer-rs-{}-{}", sanitize(base), unique_suffix())
|
||||
}
|
||||
|
||||
/// Builds the devcontainer image under `image_tag`.
|
||||
/// Construit l'image devcontainer sous `image_tag`.
|
||||
pub async fn build(
|
||||
&self,
|
||||
runtime: &ContainerRuntime,
|
||||
@@ -377,12 +377,12 @@ impl DevContainer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds the image, starts the container with the workspace mounted, runs
|
||||
/// the `postCreateCommand` / `postStartCommand` hooks with network access,
|
||||
/// then disconnects the container from the network.
|
||||
/// Construit l'image, démarre le container avec le workspace monté, exécute les
|
||||
/// hooks `postCreateCommand` / `postStartCommand` avec accès au réseau, puis
|
||||
/// déconnecte le container du réseau.
|
||||
///
|
||||
/// On any failure the container, network and image are cleaned up before
|
||||
/// returning, so no resource is leaked.
|
||||
/// En cas d'échec, le container, le réseau et l'image sont nettoyés avant de
|
||||
/// renvoyer l'erreur, afin qu'aucune ressource ne soit laissée en place.
|
||||
pub async fn up(
|
||||
&self,
|
||||
runtime: &ContainerRuntime,
|
||||
@@ -394,7 +394,7 @@ impl DevContainer {
|
||||
let name = self.container_name();
|
||||
let network = format!("{name}-net");
|
||||
|
||||
// Dedicated network so connectivity can be cut after the hooks.
|
||||
// Réseau dédié afin de pouvoir couper la connectivité après les hooks.
|
||||
let args = vec!["network".to_string(), "create".to_string(), network.clone()];
|
||||
if let Err(err) = runtime
|
||||
.run(&args)
|
||||
@@ -429,13 +429,13 @@ impl DevContainer {
|
||||
image: Some(image_tag),
|
||||
};
|
||||
|
||||
// Hooks run with network access (dependency installation, etc.).
|
||||
// Les hooks s'exécutent avec accès au réseau (installation de dépendances, etc.).
|
||||
if let Err(err) = self.run_hooks(&container).await {
|
||||
let _ = container.remove().await;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
// Cut network access for the rest of the sandbox lifetime.
|
||||
// Coupe l'accès réseau pour le reste de la durée de vie de la sandbox.
|
||||
let args = vec![
|
||||
"network".to_string(),
|
||||
"disconnect".to_string(),
|
||||
@@ -455,8 +455,8 @@ impl DevContainer {
|
||||
}
|
||||
|
||||
async fn run_hooks(&self, container: &Container) -> Result<(), ContainerError> {
|
||||
// Hooks may install dependencies, so they get the long command timeout
|
||||
// rather than the short one used for tool execution.
|
||||
// Les hooks peuvent installer des dépendances, ils utilisent donc le timeout
|
||||
// long des commandes plutôt que le court réservé à l'exécution des outils.
|
||||
let timeout = container.runtime.timeout();
|
||||
|
||||
for command in [&self.post_create_command, &self.post_start_command]
|
||||
@@ -476,7 +476,7 @@ impl DevContainer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitizes a string so it can be used as a docker image/container name.
|
||||
/// Nettoie une chaîne pour qu'elle puisse servir de nom d'image/container docker.
|
||||
fn sanitize(input: &str) -> String {
|
||||
let sanitized: String = input
|
||||
.chars()
|
||||
@@ -497,7 +497,7 @@ fn sanitize(input: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Suffix unique to a sandbox run, combining the process id and a timestamp.
|
||||
/// Suffixe unique à une exécution de sandbox, combinant l'identifiant de processus et un horodatage.
|
||||
fn unique_suffix() -> String {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -507,8 +507,8 @@ fn unique_suffix() -> String {
|
||||
format!("{}-{}", std::process::id(), nanos)
|
||||
}
|
||||
|
||||
/// Lexically normalizes a path, resolving `.` and `..` without touching the
|
||||
/// filesystem. Returns `None` if the path escapes its root.
|
||||
/// Normalise lexicalement un chemin, en résolvant `.` et `..` sans toucher au
|
||||
/// système de fichiers. Renvoie `None` si le chemin sort de sa racine.
|
||||
pub fn normalize(path: &Path) -> Option<PathBuf> {
|
||||
use std::path::Component;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user