1.2: Sandboxing #7
@@ -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
|
//! Ce module invoque un runtime de containers (`docker` ou `podman`) pour construire
|
||||||
//! the devcontainer image, start a container with the workspace mounted, run the
|
//! l'image devcontainer, démarrer un container avec le workspace monté, exécuter les
|
||||||
//! `postCreateCommand` / `postStartCommand` hooks and execute commands inside the
|
//! hooks `postCreateCommand` / `postStartCommand` et lancer des commandes à l'intérieur
|
||||||
//! running container.
|
//! du container en cours d'exécution.
|
||||||
//!
|
//!
|
||||||
//! It is intentionally runtime-agnostic: any binary exposing the `docker` CLI
|
//! Il est volontairement agnostique du runtime : tout binaire exposant l'interface
|
||||||
//! surface (including `podman`) can be used via [`ContainerRuntime::new`].
|
//! CLI `docker` (y compris `podman`) peut être utilisé via [`ContainerRuntime::new`].
|
||||||
//!
|
//!
|
||||||
//! # Isolation
|
//! # Isolation
|
||||||
//!
|
//!
|
||||||
//! Each sandbox gets its own image tag, its own container and its own network.
|
//! Chaque sandbox dispose de son propre tag d'image, de son propre container et de
|
||||||
//! The container starts attached to that network so the `postCreateCommand` /
|
//! son propre réseau. Le container démarre attaché à ce réseau afin que les hooks
|
||||||
//! `postStartCommand` hooks can fetch dependencies (e.g. `npm install`); once the
|
//! `postCreateCommand` / `postStartCommand` puissent récupérer des dépendances
|
||||||
//! hooks have run, the container is disconnected from the network for the rest of
|
//! (par ex. `npm install`) ; une fois les hooks exécutés, le container est déconnecté
|
||||||
//! its lifetime. Every command is bounded by a timeout.
|
//! du réseau pour le reste de sa durée de vie. Chaque commande est bornée par un timeout.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@@ -26,16 +26,16 @@ use tokio::process::Command;
|
|||||||
|
|
||||||
use crate::DevContainer;
|
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);
|
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);
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ExecOutput {
|
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 status: i32,
|
||||||
pub stdout: String,
|
pub stdout: String,
|
||||||
pub stderr: String,
|
pub stderr: String,
|
||||||
@@ -46,7 +46,7 @@ impl ExecOutput {
|
|||||||
self.status == 0
|
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> {
|
pub fn ensure_success(self, program: &str, args: &[String]) -> Result<Self, ContainerError> {
|
||||||
if self.success() {
|
if self.success() {
|
||||||
return Ok(self);
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ContainerRuntime {
|
pub struct ContainerRuntime {
|
||||||
program: String,
|
program: String,
|
||||||
@@ -108,7 +108,7 @@ impl ContainerRuntime {
|
|||||||
Self::new("podman")
|
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 {
|
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||||
self.timeout = timeout;
|
self.timeout = timeout;
|
||||||
self
|
self
|
||||||
@@ -122,7 +122,7 @@ impl ContainerRuntime {
|
|||||||
self.timeout
|
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 {
|
pub async fn available(&self) -> bool {
|
||||||
Command::new(&self.program)
|
Command::new(&self.program)
|
||||||
.arg("version")
|
.arg("version")
|
||||||
@@ -135,15 +135,15 @@ impl ContainerRuntime {
|
|||||||
.unwrap_or(false)
|
.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
|
/// Seuls les échecs de lancement (spawn) et les timeouts sont des erreurs ; un
|
||||||
/// code is returned in the [`ExecOutput`] so callers can decide how to react.
|
/// code non nul est renvoyé dans [`ExecOutput`] afin que les appelants réagissent.
|
||||||
pub async fn run(&self, args: &[String]) -> Result<ExecOutput, ContainerError> {
|
pub async fn run(&self, args: &[String]) -> Result<ExecOutput, ContainerError> {
|
||||||
self.run_with_timeout(args, self.timeout).await
|
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(
|
pub async fn run_with_timeout(
|
||||||
&self,
|
&self,
|
||||||
args: &[String],
|
args: &[String],
|
||||||
@@ -174,7 +174,7 @@ impl ContainerRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A running devcontainer.
|
/// Un devcontainer en cours d'exécution.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Container {
|
pub struct Container {
|
||||||
runtime: ContainerRuntime,
|
runtime: ContainerRuntime,
|
||||||
@@ -194,10 +194,10 @@ impl Container {
|
|||||||
&self.workspace_folder
|
&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
|
/// La commande est transmise sous forme de vecteur d'arguments (argv, sans shell),
|
||||||
/// interpolation is performed.
|
/// donc aucun échappement ni interpolation n'est effectué.
|
||||||
pub async fn exec(&self, cmd: &[&str]) -> Result<ExecOutput, ContainerError> {
|
pub async fn exec(&self, cmd: &[&str]) -> Result<ExecOutput, ContainerError> {
|
||||||
self.exec_with_timeout(cmd, DEFAULT_EXEC_TIMEOUT).await
|
self.exec_with_timeout(cmd, DEFAULT_EXEC_TIMEOUT).await
|
||||||
}
|
}
|
||||||
@@ -220,12 +220,12 @@ impl Container {
|
|||||||
self.runtime.run_with_timeout(&args, timeout).await
|
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> {
|
pub async fn exec_shell(&self, script: &str) -> Result<ExecOutput, ContainerError> {
|
||||||
self.exec(&["sh", "-c", script]).await
|
self.exec(&["sh", "-c", script]).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops the container.
|
/// Arrête le container.
|
||||||
pub async fn stop(&self) -> Result<(), ContainerError> {
|
pub async fn stop(&self) -> Result<(), ContainerError> {
|
||||||
let args = vec!["stop".to_string(), self.name.clone()];
|
let args = vec!["stop".to_string(), self.name.clone()];
|
||||||
self.runtime
|
self.runtime
|
||||||
@@ -235,9 +235,9 @@ impl Container {
|
|||||||
Ok(())
|
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> {
|
pub async fn remove(&self) -> Result<(), ContainerError> {
|
||||||
let args = vec![
|
let args = vec![
|
||||||
"rm".to_string(),
|
"rm".to_string(),
|
||||||
@@ -265,21 +265,21 @@ impl Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DevContainer {
|
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 {
|
pub fn image_name(&self) -> String {
|
||||||
let base = self.name.as_deref().unwrap_or("devcontainer");
|
let base = self.name.as_deref().unwrap_or("devcontainer");
|
||||||
format!("devcontainer-rs/{}", sanitize(base))
|
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
|
/// L'unicité est importante : deux sandboxes concurrentes (éventuellement pour des
|
||||||
/// repositories sharing a devcontainer name) must not race on a shared tag.
|
/// 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 {
|
pub fn image_tag(&self) -> String {
|
||||||
format!("{}:{}", self.image_name(), unique_suffix())
|
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> {
|
pub fn build_args(&self, image_tag: &str) -> Vec<String> {
|
||||||
let context = self
|
let context = self
|
||||||
.container_file_path
|
.container_file_path
|
||||||
@@ -302,7 +302,7 @@ impl DevContainer {
|
|||||||
args
|
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(
|
pub fn run_args(
|
||||||
&self,
|
&self,
|
||||||
workspace_dir: &Path,
|
workspace_dir: &Path,
|
||||||
@@ -340,27 +340,27 @@ impl DevContainer {
|
|||||||
args.extend(self.run_args.iter().cloned());
|
args.extend(self.run_args.iter().cloned());
|
||||||
|
qpismont marked this conversation as resolved
Outdated
|
|||||||
|
|
||||||
args.push(image_tag.to_string());
|
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("sleep".to_string());
|
||||||
args.push("infinity".to_string());
|
args.push("infinity".to_string());
|
||||||
|
|
||||||
args
|
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 {
|
pub fn workspace_folder(&self) -> String {
|
||||||
self.workspace_folder
|
self.workspace_folder
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "/workspaces/workspace".to_string())
|
.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 {
|
pub fn container_name(&self) -> String {
|
||||||
let base = self.name.as_deref().unwrap_or("devcontainer");
|
let base = self.name.as_deref().unwrap_or("devcontainer");
|
||||||
format!("devcontainer-rs-{}-{}", sanitize(base), unique_suffix())
|
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(
|
pub async fn build(
|
||||||
&self,
|
&self,
|
||||||
runtime: &ContainerRuntime,
|
runtime: &ContainerRuntime,
|
||||||
@@ -377,12 +377,12 @@ impl DevContainer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the image, starts the container with the workspace mounted, runs
|
/// Construit l'image, démarre le container avec le workspace monté, exécute les
|
||||||
/// the `postCreateCommand` / `postStartCommand` hooks with network access,
|
/// hooks `postCreateCommand` / `postStartCommand` avec accès au réseau, puis
|
||||||
/// then disconnects the container from the network.
|
/// déconnecte le container du réseau.
|
||||||
///
|
///
|
||||||
/// On any failure the container, network and image are cleaned up before
|
/// En cas d'échec, le container, le réseau et l'image sont nettoyés avant de
|
||||||
/// returning, so no resource is leaked.
|
/// renvoyer l'erreur, afin qu'aucune ressource ne soit laissée en place.
|
||||||
pub async fn up(
|
pub async fn up(
|
||||||
&self,
|
&self,
|
||||||
runtime: &ContainerRuntime,
|
runtime: &ContainerRuntime,
|
||||||
@@ -394,7 +394,7 @@ impl DevContainer {
|
|||||||
let name = self.container_name();
|
let name = self.container_name();
|
||||||
let network = format!("{name}-net");
|
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()];
|
let args = vec!["network".to_string(), "create".to_string(), network.clone()];
|
||||||
if let Err(err) = runtime
|
if let Err(err) = runtime
|
||||||
.run(&args)
|
.run(&args)
|
||||||
@@ -429,13 +429,13 @@ impl DevContainer {
|
|||||||
image: Some(image_tag),
|
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 {
|
if let Err(err) = self.run_hooks(&container).await {
|
||||||
let _ = container.remove().await;
|
let _ = container.remove().await;
|
||||||
return Err(err);
|
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![
|
let args = vec![
|
||||||
"network".to_string(),
|
"network".to_string(),
|
||||||
"disconnect".to_string(),
|
"disconnect".to_string(),
|
||||||
@@ -455,8 +455,8 @@ impl DevContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_hooks(&self, container: &Container) -> Result<(), ContainerError> {
|
async fn run_hooks(&self, container: &Container) -> Result<(), ContainerError> {
|
||||||
// Hooks may install dependencies, so they get the long command timeout
|
// Les hooks peuvent installer des dépendances, ils utilisent donc le timeout
|
||||||
// rather than the short one used for tool execution.
|
// long des commandes plutôt que le court réservé à l'exécution des outils.
|
||||||
let timeout = container.runtime.timeout();
|
let timeout = container.runtime.timeout();
|
||||||
|
|
||||||
for command in [&self.post_create_command, &self.post_start_command]
|
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 {
|
fn sanitize(input: &str) -> String {
|
||||||
|
qpismont marked this conversation as resolved
Herald
commented
La fonction La fonction `sanitize` tronque les tirets et points en début et fin, mais elle pourrait laisser passer des séquences de deux tirets consécutifs qui sont autorisées par Docker. Ce n’est pas critique, mais une validation plus stricte (limiter la longueur, interdire les séquences interdites) améliorerait la robustesse.
|
|||||||
let sanitized: String = input
|
let sanitized: String = input
|
||||||
.chars()
|
.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 {
|
fn unique_suffix() -> String {
|
||||||
let nanos = SystemTime::now()
|
let nanos = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -507,8 +507,8 @@ fn unique_suffix() -> String {
|
|||||||
format!("{}-{}", std::process::id(), nanos)
|
format!("{}-{}", std::process::id(), nanos)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lexically normalizes a path, resolving `.` and `..` without touching the
|
/// Normalise lexicalement un chemin, en résolvant `.` et `..` sans toucher au
|
||||||
/// filesystem. Returns `None` if the path escapes its root.
|
/// système de fichiers. Renvoie `None` si le chemin sort de sa racine.
|
||||||
pub fn normalize(path: &Path) -> Option<PathBuf> {
|
pub fn normalize(path: &Path) -> Option<PathBuf> {
|
||||||
use std::path::Component;
|
use std::path::Component;
|
||||||
|
|
||||||
|
|||||||
@@ -119,9 +119,9 @@ impl TryFrom<(DevContainerSchema, PathBuf)> for DevContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves `${localEnv:VAR}` and `${localEnv:VAR:default}` references using the
|
/// Résout les références `${localEnv:VAR}` et `${localEnv:VAR:default}` à l'aide de
|
||||||
/// current process environment, as described by the devcontainer specification.
|
/// l'environnement du processus courant, comme décrit par la spécification devcontainer.
|
||||||
/// Unresolved variables without a default expand to an empty string.
|
/// Les variables non résolues sans valeur par défaut sont remplacées par une chaîne vide.
|
||||||
fn substitute_local_env(input: &str) -> String {
|
fn substitute_local_env(input: &str) -> String {
|
||||||
const PREFIX: &str = "${localEnv:";
|
const PREFIX: &str = "${localEnv:";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user
Les
runArgsproviennent dudevcontainer.jsondu dépôt analysé, donc d'une PR potentiellement issue d'un fork : ils sont concaténés tels quels aux arguments dedocker run. UnrunArgsdu type["--privileged", "-v", "/:/host"](ou--pid=host,--cap-add=SYS_ADMIN) monte l'hôte dans le conteneur et annule totalement l'isolation annoncée. Comme ils sont ajoutés après le--networkposé ligne 326, ils peuvent aussi le surcharger (--network host). Prévoir une liste blanche d'options (ou ignorerrunArgs/containerEnvpour les dépôts non fiables) avant de démarrer le conteneur.