1 parent
7493de6952
commit
3078ba5705
45 files changed
+2139
-1880
No files matched your search
@@ -2,12 +2,13 @@
|
||||
name = "devcontainer-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
bollard = "0.21"
|
||||
bollard = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
tar = "0.4"
|
||||
tar = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
@@ -17,4 +18,4 @@ anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
tempfile = { workspace = true }
|
||||
@@ -1,25 +1,24 @@
|
||||
//! Constantes de la crate : endpoint du daemon, timeouts et découpage du
|
||||
//! contexte de build.
|
||||
//! Crate constants: daemon endpoint, timeouts and build context chunking.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
/// Endpoint affiché dans les logs quand `DOCKER_HOST` n'est pas défini.
|
||||
/// Endpoint shown in the logs when `DOCKER_HOST` is not set.
|
||||
pub(crate) const DEFAULT_ENDPOINT: &str = "unix:///var/run/docker.sock";
|
||||
|
||||
/// Timeout appliqué aux opérations de build/run/stop/remove.
|
||||
/// Timeout applied to build/run/stop/remove operations.
|
||||
pub(crate) const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(600);
|
||||
|
||||
/// Timeout appliqué aux commandes exécutées dans un container en cours d'exécution.
|
||||
/// Timeout applied to commands run inside a running container.
|
||||
pub(crate) const DEFAULT_EXEC_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Taille des morceaux du contexte de build envoyés au daemon.
|
||||
/// Size of the build context chunks sent to the daemon.
|
||||
pub(crate) const CONTEXT_CHUNK_SIZE: usize = 64 * 1024;
|
||||
|
||||
/// Nombre de morceaux pouvant attendre dans le canal : c'est ce qui borne la
|
||||
/// mémoire occupée par le contexte, quelle que soit sa taille.
|
||||
/// Number of chunks that can wait in the channel: this is what bounds the
|
||||
/// memory held by the context, regardless of its size.
|
||||
pub(crate) const CONTEXT_CHUNKS: usize = 4;
|
||||
|
||||
/// Lectures successives du code de sortie d'un exec, et attente entre elles.
|
||||
/// Successive reads of an exec's exit code, and the delay between them.
|
||||
pub(crate) const EXIT_CODE_ATTEMPTS: usize = 10;
|
||||
pub(crate) const EXIT_CODE_DELAY: Duration = Duration::from_millis(20);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Un devcontainer en cours d'exécution : inspection et exécution de commandes.
|
||||
//! A running devcontainer: inspection and command execution.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
runtime::ContainerRuntime,
|
||||
};
|
||||
|
||||
/// Un devcontainer en cours d'exécution.
|
||||
/// A running devcontainer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Container {
|
||||
runtime: ContainerRuntime,
|
||||
@@ -19,8 +19,8 @@ pub struct Container {
|
||||
}
|
||||
|
||||
impl Container {
|
||||
/// Assemble un container démarré, avec son réseau et, le cas échéant, l'image
|
||||
/// construite pour lui et à nettoyer.
|
||||
/// Assembles a started container, with its network and, if any, the image
|
||||
/// built for it that must be cleaned up.
|
||||
pub(crate) fn new(
|
||||
runtime: ContainerRuntime,
|
||||
name: String,
|
||||
@@ -47,10 +47,10 @@ impl Container {
|
||||
&self.workspace_folder
|
||||
}
|
||||
|
||||
/// Exécute une commande dans le container et renvoie sa sortie.
|
||||
/// Runs a command in the container and returns its output.
|
||||
///
|
||||
/// La commande est transmise sous forme de vecteur d'arguments (argv, sans shell),
|
||||
/// donc aucun échappement ni interpolation n'est effectué.
|
||||
/// The command is passed as an argument vector (argv, no shell), so no
|
||||
/// escaping or interpolation is performed.
|
||||
pub async fn exec(&self, cmd: &[&str]) -> Result<ExecOutput, ContainerError> {
|
||||
self.exec_with_timeout(cmd, DEFAULT_EXEC_TIMEOUT).await
|
||||
}
|
||||
@@ -65,19 +65,19 @@ impl Container {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Exécute un script shell dans le container via `sh -c`.
|
||||
/// Runs a shell script in the container via `sh -c`.
|
||||
pub async fn exec_shell(&self, script: &str) -> Result<ExecOutput, ContainerError> {
|
||||
self.exec(&["sh", "-c", script]).await
|
||||
}
|
||||
|
||||
/// Arrête le container.
|
||||
/// Stops the container.
|
||||
pub async fn stop(&self) -> Result<(), ContainerError> {
|
||||
self.runtime.stop_container(&self.name).await
|
||||
}
|
||||
|
||||
/// Supprime le container, son réseau et son image.
|
||||
/// Removes the container, its network and its image.
|
||||
///
|
||||
/// La suppression du réseau et de l'image est best-effort : ils peuvent déjà être absents.
|
||||
/// Removing the network and the image is best-effort: they may already be gone.
|
||||
pub async fn remove(&self) -> Result<(), ContainerError> {
|
||||
self.runtime.remove_container(&self.name).await?;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Empaquetage en flux du contexte de build envoyé au daemon.
|
||||
//! Streaming packaging of the build context sent to the daemon.
|
||||
|
||||
use std::{
|
||||
io::{BufWriter, Write},
|
||||
@@ -12,13 +12,13 @@ use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
use crate::consts::{CONTEXT_CHUNK_SIZE, CONTEXT_CHUNKS};
|
||||
|
||||
/// Empaquette le contexte de build dans un tar, **en flux**.
|
||||
/// Packs the build context into a tar, **as a stream**.
|
||||
///
|
||||
/// Le CLI `docker build` empaquette le contexte puis l'envoie ; le faire d'un coup
|
||||
/// chargerait tout le dossier en mémoire, ce qui devient intenable dès que le
|
||||
/// `.devcontainer` embarque des binaires. Ici l'archive est écrite par une tâche
|
||||
/// bloquante et poussée morceau par morceau, avec la contre-pression du canal :
|
||||
/// si le daemon lit lentement, l'empaquetage ralentit.
|
||||
/// The `docker build` CLI packs the context and then sends it; doing that in one go
|
||||
/// would load the whole folder into memory, which becomes untenable as soon as the
|
||||
/// `.devcontainer` ships binaries. Here the archive is written by a blocking task
|
||||
/// and pushed chunk by chunk, with the channel's backpressure: if the daemon reads
|
||||
/// slowly, packing slows down.
|
||||
pub(crate) fn tar_directory_stream(
|
||||
dir: &Path,
|
||||
) -> impl Stream<Item = Result<Bytes, std::io::Error>> + Send + 'static {
|
||||
@@ -33,13 +33,13 @@ pub(crate) fn tar_directory_stream(
|
||||
let result = builder
|
||||
.append_dir_all(".", &dir)
|
||||
.and_then(|()| builder.finish())
|
||||
// `finish` écrit les blocs de fin d'archive ; il reste à vider le tampon
|
||||
// pour que le daemon reçoive tout, y compris une archive vide.
|
||||
// `finish` writes the end-of-archive blocks; the buffer still has to be
|
||||
// flushed so the daemon receives everything, even an empty archive.
|
||||
.and_then(|()| builder.get_mut().flush());
|
||||
|
||||
if let Err(error) = result {
|
||||
// L'échec est remonté au daemon par le flux : sinon il ne verrait qu'un
|
||||
// tar tronqué et signalerait une erreur incompréhensible.
|
||||
// The failure is reported to the daemon through the stream: otherwise it
|
||||
// would only see a truncated tar and report a confusing error.
|
||||
let _ = failures.blocking_send(Err(error));
|
||||
}
|
||||
});
|
||||
@@ -47,9 +47,9 @@ pub(crate) fn tar_directory_stream(
|
||||
ReceiverStream::new(receiver)
|
||||
}
|
||||
|
||||
/// Écrit les morceaux du tar dans un canal, en attendant qu'il se vide.
|
||||
/// Writes the tar chunks into a channel, waiting for it to drain.
|
||||
///
|
||||
/// Utilisé depuis une tâche bloquante, seul contexte où `blocking_send` est autorisé.
|
||||
/// Used from a blocking task, the only context where `blocking_send` is allowed.
|
||||
struct ChannelWriter {
|
||||
sender: mpsc::Sender<Result<Bytes, std::io::Error>>,
|
||||
}
|
||||
@@ -80,7 +80,7 @@ mod tests {
|
||||
|
||||
use futures_util::StreamExt;
|
||||
|
||||
/// Rassemble le flux du contexte en un tar complet, pour l'inspecter.
|
||||
/// Collects the context stream into a complete tar, for inspection.
|
||||
async fn context_tar(dir: &Path) -> Vec<u8> {
|
||||
let mut tar = Vec::new();
|
||||
let mut chunks = tar_directory_stream(dir);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Représentation analysée d'un `devcontainer.json` et son cycle de vie :
|
||||
//! nommage, build, démarrage et hooks.
|
||||
//! Parsed representation of a `devcontainer.json` and its lifecycle: naming,
|
||||
//! build, startup and hooks.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -29,29 +29,29 @@ pub struct DevContainer {
|
||||
}
|
||||
|
||||
impl DevContainer {
|
||||
/// Dossier de workspace dans le container, par défaut `/workspace`.
|
||||
/// Workspace folder inside the container, `/workspace` by default.
|
||||
pub fn workspace_folder(&self) -> String {
|
||||
self.workspace_folder
|
||||
.clone()
|
||||
.unwrap_or_else(|| "/workspace".to_string())
|
||||
}
|
||||
|
||||
/// Tag de l'image construite localement à partir du Dockerfile.
|
||||
/// Tag of the image built locally from the Dockerfile.
|
||||
fn local_image_tag(&self, id: &str) -> String {
|
||||
format!("devcontainer-rs/{}:{}", self.sanitized_name(), id)
|
||||
}
|
||||
|
||||
/// Nom du container de cette sandbox.
|
||||
/// Name of this sandbox's container.
|
||||
fn container_name(&self, id: &str) -> String {
|
||||
format!("devcontainer-rs-{}-{}", self.sanitized_name(), id)
|
||||
}
|
||||
|
||||
/// Nom du devcontainer nettoyé pour servir de nom d'image/container.
|
||||
/// Devcontainer name, sanitized to be usable as an image/container name.
|
||||
fn sanitized_name(&self) -> String {
|
||||
sanitize(self.name.as_deref().unwrap_or("devcontainer"))
|
||||
}
|
||||
|
||||
/// Construit l'image devcontainer sous `image_tag`.
|
||||
/// Builds the devcontainer image as `image_tag`.
|
||||
async fn build(
|
||||
&self,
|
||||
runtime: &ContainerRuntime,
|
||||
@@ -239,7 +239,7 @@ impl Default for DevContainer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Nettoie une chaîne pour qu'elle puisse servir de nom d'image/container docker.
|
||||
/// Sanitizes a string so it can be used as a docker image/container name.
|
||||
fn sanitize(input: &str) -> String {
|
||||
let sanitized: String = input
|
||||
.chars()
|
||||
@@ -260,7 +260,7 @@ fn sanitize(input: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifiant unique à une exécution de sandbox, combinant l'identifiant de processus et un horodatage.
|
||||
/// Identifier unique to a sandbox run, combining the process id and a timestamp.
|
||||
fn unique_id() -> String {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Erreurs de la crate : échecs du daemon de containers et échecs d'analyse du
|
||||
//! `devcontainer.json`.
|
||||
//! Crate errors: container daemon failures and `devcontainer.json` parsing failures.
|
||||
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
@@ -7,25 +6,25 @@ use bollard::errors::Error as BollardError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ContainerError {
|
||||
/// Le daemon est injoignable, ou a refusé une requête.
|
||||
/// The daemon is unreachable, or rejected a request.
|
||||
#[error("container daemon request failed: {source}")]
|
||||
Request {
|
||||
#[from]
|
||||
source: BollardError,
|
||||
},
|
||||
|
||||
/// Une opération a dépassé son timeout.
|
||||
/// An operation exceeded its timeout.
|
||||
#[error("`{operation}` timed out after {timeout:?}")]
|
||||
Timeout {
|
||||
operation: String,
|
||||
timeout: Duration,
|
||||
},
|
||||
|
||||
/// La construction de l'image a échoué.
|
||||
/// The image build failed.
|
||||
#[error("image build failed: {message}")]
|
||||
Build { message: String },
|
||||
|
||||
/// Le daemon a répondu autre chose que ce qui était attendu.
|
||||
/// The daemon replied with something other than what was expected.
|
||||
#[error("{0}")]
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Résultat d'une commande exécutée dans un container.
|
||||
//! Result of a command run in a container.
|
||||
|
||||
/// Résultat d'une commande exécutée dans un container.
|
||||
/// Result of a command run in a container.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecOutput {
|
||||
/// Code de sortie, ou `-1` si le processus a été terminé par un signal.
|
||||
/// Exit code, or `-1` if the process was terminated by a signal.
|
||||
pub status: i32,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
//! Primitives de cycle de vie de container pour un [`DevContainer`] analysé.
|
||||
//! Container lifecycle primitives for a parsed [`DevContainer`].
|
||||
//!
|
||||
//! Cette crate pilote l'API du daemon de containers pour préparer l'image
|
||||
//! devcontainer (construite depuis un Dockerfile, ou tirée d'un registre),
|
||||
//! démarrer un container et y copier le workspace, exécuter les
|
||||
//! hooks `postCreateCommand` / `postStartCommand` et lancer des commandes à
|
||||
//! l'intérieur du container en cours d'exécution.
|
||||
//! This crate drives the container daemon API to prepare the devcontainer image
|
||||
//! (built from a Dockerfile, or pulled from a registry), start a container and
|
||||
//! copy the workspace into it, run the `postCreateCommand` / `postStartCommand`
|
||||
//! hooks, and run commands inside the running container.
|
||||
//!
|
||||
//! Le daemon est joint par son **socket** (celui de l'hôte, monté dans le
|
||||
//! container de Steep) : [`ContainerRuntime::connect`] suit `DOCKER_HOST` comme
|
||||
//! le fait le CLI `docker`, et retombe sur le socket local. Aucun binaire de
|
||||
//! runtime n'est donc requis dans l'image, et `podman` fonctionne de la même
|
||||
//! façon dès lors qu'il expose son socket compatible Docker.
|
||||
//! The daemon is reached through its **socket** (the host's, mounted into the
|
||||
//! Steep container): [`ContainerRuntime::connect`] follows `DOCKER_HOST` like the
|
||||
//! `docker` CLI does, and falls back to the local socket. No runtime binary is
|
||||
//! therefore required in the image, and `podman` works the same way as long as it
|
||||
//! exposes its Docker-compatible socket.
|
||||
//!
|
||||
//! # Isolation
|
||||
//!
|
||||
//! 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.
|
||||
//! Each sandbox has its own image tag, its own container and its own network. The
|
||||
//! container starts attached to that network so that 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.
|
||||
//!
|
||||
//! Les `runArgs` du `devcontainer.json` sont ignorés : ils viennent du dépôt, donc
|
||||
//! d'une pull request non fiable, et pourraient rattacher le container à un autre
|
||||
//! réseau ou lui donner des privilèges qui annuleraient cette isolation.
|
||||
//! The `devcontainer.json` `runArgs` are ignored: they come from the repository,
|
||||
//! hence from an untrusted pull request, and could attach the container to another
|
||||
//! network or grant it privileges that would defeat this isolation.
|
||||
|
||||
mod consts;
|
||||
mod container;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Normalisation lexicale de chemins, sans accès au système de fichiers.
|
||||
//! Lexical path normalization, without accessing the filesystem.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Normalise lexicalement un chemin, en résolvant `.` et `..` sans toucher au
|
||||
/// système de fichiers. Renvoie `None` si le chemin sort de sa racine.
|
||||
/// Lexically normalizes a path, resolving `.` and `..` without touching the
|
||||
/// filesystem. Returns `None` if the path escapes its root.
|
||||
pub fn normalize(path: &Path) -> Option<PathBuf> {
|
||||
use std::path::Component;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Client de l'API du daemon de containers et opérations de cycle de vie.
|
||||
//! Container daemon API client and lifecycle operations.
|
||||
|
||||
use std::{collections::HashMap, path::Path, time::Duration};
|
||||
|
||||
@@ -24,7 +24,7 @@ use crate::{
|
||||
exec::ExecOutput,
|
||||
};
|
||||
|
||||
/// Un client de l'API du daemon de containers.
|
||||
/// A container daemon API client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContainerRuntime {
|
||||
docker: Docker,
|
||||
@@ -33,7 +33,7 @@ pub struct ContainerRuntime {
|
||||
}
|
||||
|
||||
impl ContainerRuntime {
|
||||
/// Se connecte au daemon désigné par `DOCKER_HOST`, ou au socket local par défaut.
|
||||
/// Connects to the daemon designated by `DOCKER_HOST`, or to the default local socket.
|
||||
pub fn connect() -> Result<Self, ContainerError> {
|
||||
let endpoint =
|
||||
std::env::var("DOCKER_HOST").unwrap_or_else(|_| String::from(DEFAULT_ENDPOINT));
|
||||
@@ -45,13 +45,13 @@ impl ContainerRuntime {
|
||||
})
|
||||
}
|
||||
|
||||
/// Remplace le timeout appliqué aux opérations de build/run/stop/remove.
|
||||
/// Replaces the timeout applied to build/run/stop/remove operations.
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Endpoint du daemon, pour les logs.
|
||||
/// Daemon endpoint, for logging.
|
||||
pub fn endpoint(&self) -> &str {
|
||||
&self.endpoint
|
||||
}
|
||||
@@ -60,12 +60,12 @@ impl ContainerRuntime {
|
||||
self.timeout
|
||||
}
|
||||
|
||||
/// Vérifie que le daemon est joignable et répond.
|
||||
/// Checks that the daemon is reachable and responding.
|
||||
pub async fn available(&self) -> bool {
|
||||
self.docker.ping().await.is_ok()
|
||||
}
|
||||
|
||||
/// Exécute une requête du daemon en appliquant le timeout des commandes.
|
||||
/// Runs a daemon request, applying the command timeout.
|
||||
async fn request<T>(
|
||||
&self,
|
||||
operation: &str,
|
||||
@@ -81,7 +81,7 @@ impl ContainerRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit l'image `tag` depuis le contexte `context_dir`.
|
||||
/// Builds the image `tag` from the `context_dir` context.
|
||||
pub(crate) async fn build_image(
|
||||
&self,
|
||||
context_dir: &Path,
|
||||
@@ -102,8 +102,8 @@ impl ContainerRuntime {
|
||||
.docker
|
||||
.build_image(options, None, Some(body_try_stream(context)));
|
||||
|
||||
// Le flux porte la progression du build : la dernière erreur signalée par le
|
||||
// daemon fait échouer l'opération.
|
||||
// The stream carries the build progress: the last error reported by the
|
||||
// daemon makes the operation fail.
|
||||
let consume = async {
|
||||
let mut failure = None;
|
||||
|
||||
@@ -162,7 +162,7 @@ impl ContainerRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un container, sans le démarrer.
|
||||
/// Creates a container without starting it.
|
||||
pub(crate) async fn create_container(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -217,7 +217,7 @@ impl ContainerRuntime {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Exécute une commande dans un container et renvoie sa sortie.
|
||||
/// Runs a command in a container and returns its output.
|
||||
pub(crate) async fn exec(
|
||||
&self,
|
||||
container: &str,
|
||||
@@ -254,8 +254,8 @@ impl ContainerRuntime {
|
||||
let mut stdout = String::new();
|
||||
let mut stderr = String::new();
|
||||
|
||||
// Le timeout couvre l'exécution de la commande elle-même, pas seulement sa
|
||||
// mise en place.
|
||||
// The timeout covers the execution of the command itself, not just its
|
||||
// setup.
|
||||
let collect = async {
|
||||
while let Some(message) = output.next().await {
|
||||
match message? {
|
||||
@@ -276,10 +276,10 @@ impl ContainerRuntime {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(source)) => return Err(ContainerError::Request { source }),
|
||||
Err(_) => {
|
||||
// Le processus continue de tourner dans le container : sans arrêt, il
|
||||
// consommerait CPU et mémoire et pourrait encore modifier le workspace
|
||||
// pendant toute la durée de vie de la sandbox. L'API n'offre pas de
|
||||
// moyen de tuer un exec, on arrête donc le container qui le porte.
|
||||
// The process keeps running in the container: left alone, it would
|
||||
// consume CPU and memory and could still modify the workspace for the
|
||||
// whole lifetime of the sandbox. The API offers no way to kill an exec,
|
||||
// so we stop the container that hosts it.
|
||||
let _ = self.stop_container(container).await;
|
||||
|
||||
return Err(ContainerError::Timeout {
|
||||
@@ -289,10 +289,10 @@ impl ContainerRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
// Le code de sortie n'est pas forcément publié au moment où le flux se ferme :
|
||||
// on laisse au daemon le temps de le renseigner, sans bloquer indéfiniment.
|
||||
// Sans cela, une commande réussie peut être rapportée en échec (code -1) et
|
||||
// l'outil renvoie une erreur au modèle à la place du contenu.
|
||||
// The exit code is not necessarily published by the time the stream closes:
|
||||
// we give the daemon time to fill it in, without blocking indefinitely.
|
||||
// Without this, a successful command can be reported as failed (code -1) and
|
||||
// the tool returns an error to the model instead of the content.
|
||||
let mut inspected = self
|
||||
.request("inspect exec", self.docker.inspect_exec(&created.id))
|
||||
.await?;
|
||||
@@ -331,7 +331,7 @@ impl ContainerRuntime {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Supprime un container, ses volumes anonymes et le processus qui y tourne.
|
||||
/// Removes a container, its anonymous volumes and the process running in it.
|
||||
pub(crate) async fn remove_container(&self, name: &str) -> Result<(), ContainerError> {
|
||||
let options = RemoveContainerOptions {
|
||||
force: true,
|
||||
@@ -363,7 +363,7 @@ impl ContainerRuntime {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Crée le réseau dédié d'une sandbox.
|
||||
/// Creates a sandbox's dedicated network.
|
||||
pub(crate) async fn create_network(&self, name: &str) -> Result<(), ContainerError> {
|
||||
let request = NetworkCreateRequest {
|
||||
name: String::from(name),
|
||||
@@ -376,7 +376,7 @@ impl ContainerRuntime {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Détache un container d'un réseau, ce qui coupe sa connectivité.
|
||||
/// Detaches a container from a network, which cuts off its connectivity.
|
||||
pub(crate) async fn disconnect_network(
|
||||
&self,
|
||||
network: &str,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Schéma du `devcontainer.json`, analyse du fichier et résolution des
|
||||
//! références `${localEnv:…}`.
|
||||
//! `devcontainer.json` schema, file parsing and resolution of `${localEnv:…}`
|
||||
//! references.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -23,7 +23,7 @@ struct DevContainerSchema {
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
|
||||
/// Image de base tirée du registre, alternative à `build`.
|
||||
/// Base image pulled from the registry, alternative to `build`.
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
|
||||
@@ -106,16 +106,16 @@ impl TryFrom<(DevContainerSchema, PathBuf)> for DevContainer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Résout les références `${localEnv:VAR}` et `${localEnv:VAR:default}` d'un
|
||||
/// Resolves the `${localEnv:VAR}` and `${localEnv:VAR:default}` references of a
|
||||
/// `devcontainer.json`.
|
||||
///
|
||||
/// La spécification veut que `${localEnv:VAR}` soit lu dans l'environnement du
|
||||
/// client ; ici le `devcontainer.json` vient d'une **pull request**, donc de code
|
||||
/// non fiable : lire l'environnement du processus lui permettrait de récupérer
|
||||
/// `GITEA_TOKEN`, `OPEN_ROUTER_API_KEY` ou n'importe quel autre secret de Steep et
|
||||
/// de l'exfiltrer depuis un `build.args`, un `containerEnv`, son Dockerfile ou un
|
||||
/// hook. L'environnement n'est donc **jamais** consulté : seule la valeur par
|
||||
/// défaut est utilisée, et une référence sans défaut devient une chaîne vide.
|
||||
/// The specification says `${localEnv:VAR}` is read from the client's environment;
|
||||
/// here the `devcontainer.json` comes from a **pull request**, hence from untrusted
|
||||
/// code: reading the process environment would let it grab `GITEA_TOKEN`,
|
||||
/// `OPEN_ROUTER_API_KEY` or any other Steep secret and exfiltrate it through a
|
||||
/// `build.args`, a `containerEnv`, its Dockerfile or a hook. The environment is
|
||||
/// therefore **never** consulted: only the default value is used, and a reference
|
||||
/// without a default becomes an empty string.
|
||||
fn substitute_local_env(input: &str) -> String {
|
||||
const PREFIX: &str = "${localEnv:";
|
||||
|
||||
@@ -129,7 +129,7 @@ fn substitute_local_env(input: &str) -> String {
|
||||
match after.find('}') {
|
||||
Some(end) => {
|
||||
let inner = &after[..end];
|
||||
// La clé est lue pour délimiter la référence, pas pour la résoudre.
|
||||
// The key is read to delimit the reference, not to resolve it.
|
||||
let (_key, default) = match inner.split_once(':') {
|
||||
Some((key, default)) => (key, Some(default)),
|
||||
None => (inner, None),
|
||||
@@ -239,8 +239,8 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let devcontainer_path = dir.path().join("devcontainer.json");
|
||||
|
||||
// `runArgs` vient d'une pull request non fiable : il doit être accepté par
|
||||
// l'analyse mais jamais repris dans le devcontainer.
|
||||
// `runArgs` comes from an untrusted pull request: it must be accepted by
|
||||
// parsing but never carried over into the devcontainer.
|
||||
fs::write(
|
||||
&devcontainer_path,
|
||||
r#"{
|
||||
@@ -271,8 +271,8 @@ mod tests {
|
||||
fn local_env_references_are_never_read_from_the_environment() {
|
||||
unsafe { std::env::set_var("DEVCONTAINER_TEST_SECRET", "s3cret") };
|
||||
|
||||
// Le `devcontainer.json` vient d'une pull request : résoudre la référence
|
||||
// depuis l'environnement de Steep y exposerait ses jetons.
|
||||
// The `devcontainer.json` comes from a pull request: resolving the reference
|
||||
// from Steep's environment would expose its tokens to it.
|
||||
assert_eq!(
|
||||
substitute_local_env("${localEnv:DEVCONTAINER_TEST_SECRET}"),
|
||||
""
|
||||
|
||||
Reference in new issue
Block a user