Starting impl devcontainer spec
This commit is contained in:
1 parent
d711153553
commit
15f619ccf7
23 files changed
+271
-30
No files matched your search
@@ -1,4 +1,4 @@
|
||||
FROM debian:trixie
|
||||
FROM rust:1.97-trixie
|
||||
|
||||
ARG USERNAME=dev
|
||||
ARG USER_UID=1000
|
||||
@@ -18,11 +18,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd --gid ${USER_GID:-1000} $USERNAME \
|
||||
&& useradd --uid ${USER_UID:-1000} --gid ${USER_GID:-1000} -m $USERNAME
|
||||
&& useradd --uid ${USER_UID:-1000} --gid ${USER_GID:-1000} -m $USERNAME \
|
||||
&& rustup component add clippy
|
||||
|
||||
|
||||
|
||||
USER $USERNAME
|
||||
WORKDIR /home/$USERNAME
|
||||
|
||||
ENV PATH="/home/${USERNAME}/.cargo/bin:${PATH}"
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
@@ -19,5 +19,6 @@
|
||||
},
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/herald,type=bind",
|
||||
"workspaceFolder": "/workspaces/herald",
|
||||
"runArgs": ["--userns=keep-id", "--security-opt", "label=disable"],
|
||||
"appPort": [3000]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
target/
|
||||
.env
|
||||
.devcontainer/
|
||||
docs/
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"rust-analyzer.check.command": "clippy"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"lsp": {
|
||||
"rust-analyzer": {
|
||||
"initialization_options": {
|
||||
"check": {
|
||||
"command": "clippy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+15
-2
@@ -490,6 +490,18 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "devcontainer-rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
@@ -787,12 +799,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "herald"
|
||||
version = "1.1.0"
|
||||
name = "herald-server"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"bytes",
|
||||
"devcontainer-rs",
|
||||
"dotenvy",
|
||||
"futures-util",
|
||||
"hex",
|
||||
|
||||
+12
-10
@@ -1,12 +1,11 @@
|
||||
[package]
|
||||
name = "herald"
|
||||
version = "1.1.0"
|
||||
edition = "2024"
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/herald-server",
|
||||
"crates/devcontainer-rs",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
[dependencies]
|
||||
[workspace.dependencies]
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
tokio = { version = "1.53", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
@@ -19,9 +18,9 @@ sentry-anyhow = { version = "0.48", features = ["backtrace"] }
|
||||
openrouter-rs = "0.12"
|
||||
dotenvy = "0.15"
|
||||
tower = "0.5"
|
||||
tower-http = {version = "0.6", features = ["trace"] }
|
||||
tower-http = { version = "0.6", features = ["trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features=["env-filter"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
axum = "0.8"
|
||||
anyhow = { version = "1.0", features = ["backtrace"] }
|
||||
thiserror = "2.0"
|
||||
@@ -30,3 +29,6 @@ hex = "0.4"
|
||||
bytes = "1.1"
|
||||
metrics = "0.24"
|
||||
metrics-exporter-prometheus = { version = "0.18", default-features = false, features = ["http-listener"] }
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
+8
-5
@@ -1,12 +1,15 @@
|
||||
FROM rust:1.96 as builder
|
||||
FROM rust:1.97-trixie as builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN cargo build --release
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/ crates/
|
||||
|
||||
RUN cargo build --release --package herald-server
|
||||
|
||||
|
||||
FROM debian:trixie-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/target/release/herald .
|
||||
CMD [ "./herald" ]
|
||||
COPY --from=builder /app/target/release/herald-server .
|
||||
CMD [ "./herald-server" ]
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "devcontainer-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@@ -0,0 +1,175 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DevContainerBuildSchema {
|
||||
#[serde(default)]
|
||||
pub dockerfile: Option<String>,
|
||||
#[serde(default)]
|
||||
pub args: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DevContainerSchema {
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
pub build: DevContainerBuildSchema,
|
||||
|
||||
#[serde(rename = "workspaceFolder", default)]
|
||||
pub workspace_folder: Option<String>,
|
||||
|
||||
#[serde(rename = "containerEnv", default)]
|
||||
pub container_env: HashMap<String, String>,
|
||||
|
||||
#[serde(rename = "postCreateCommand", default)]
|
||||
pub post_create_command: Option<String>,
|
||||
|
||||
#[serde(rename = "postStartCommand", default)]
|
||||
pub post_start_command: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DevContainer {
|
||||
/// Absolute or relative path to the Dockerfile/Containerfile to build.
|
||||
pub container_file_path: PathBuf,
|
||||
pub name: Option<String>,
|
||||
pub build_args: HashMap<String, String>,
|
||||
pub container_env: HashMap<String, String>,
|
||||
pub workspace_folder: Option<String>,
|
||||
pub post_create_command: Option<String>,
|
||||
pub post_start_command: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ParseError {
|
||||
#[error("failed to read devcontainer file `{path}`: {source}")]
|
||||
Read {
|
||||
path: PathBuf,
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("invalid devcontainer JSON in `{path}`: {source}")]
|
||||
Json {
|
||||
path: PathBuf,
|
||||
source: serde_json::Error,
|
||||
},
|
||||
|
||||
#[error("container file `{0}` does not exist or is not a regular file")]
|
||||
ContainerFileNotFound(PathBuf),
|
||||
|
||||
#[error("the devcontainer file path has no parent directory: `{0}`")]
|
||||
InvalidDevContainerPath(PathBuf),
|
||||
}
|
||||
|
||||
impl TryFrom<(DevContainerSchema, PathBuf)> for DevContainer {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from((schema, devcontainer_path): (DevContainerSchema, PathBuf)) -> Result<Self, Self::Error> {
|
||||
let base_dir = devcontainer_path
|
||||
.parent()
|
||||
.ok_or_else(|| ParseError::InvalidDevContainerPath(devcontainer_path.clone()))?;
|
||||
|
||||
let container_file_path = match schema.build.dockerfile.as_deref() {
|
||||
Some(file) => base_dir.join(file),
|
||||
None => first_existing_container_file(base_dir),
|
||||
};
|
||||
|
||||
if !container_file_path.is_file() {
|
||||
return Err(ParseError::ContainerFileNotFound(container_file_path));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
container_file_path,
|
||||
name: schema.name,
|
||||
build_args: schema.build.args,
|
||||
container_env: schema.container_env,
|
||||
workspace_folder: schema.workspace_folder,
|
||||
post_create_command: schema.post_create_command,
|
||||
post_start_command: schema.post_start_command,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn first_existing_container_file(base_dir: &Path) -> PathBuf {
|
||||
["Dockerfile", "Containerfile"]
|
||||
.iter()
|
||||
.map(|filename| base_dir.join(filename))
|
||||
.find(|path| path.is_file())
|
||||
.unwrap_or_else(|| base_dir.join("Dockerfile"))
|
||||
}
|
||||
|
||||
pub async fn parse(path: impl AsRef<Path>) -> Result<DevContainer, ParseError> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
let contents = tokio::fs::read_to_string(&path)
|
||||
.await
|
||||
.map_err(|source| ParseError::Read {
|
||||
path: path.clone(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
let schema = serde_json::from_str::<DevContainerSchema>(&contents).map_err(|source| {
|
||||
ParseError::Json {
|
||||
path: path.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
|
||||
DevContainer::try_from((schema, path))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn resolves_configured_containerfile_relative_to_devcontainer_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let devcontainer_path = dir.path().join("devcontainer.json");
|
||||
let containerfile_path = dir.path().join("Containerfile");
|
||||
fs::write(&containerfile_path, "FROM alpine\n").unwrap();
|
||||
|
||||
let schema = DevContainerSchema {
|
||||
name: Some("test".into()),
|
||||
build: DevContainerBuildSchema {
|
||||
dockerfile: Some("Containerfile".into()),
|
||||
args: HashMap::new(),
|
||||
},
|
||||
workspace_folder: None,
|
||||
container_env: HashMap::new(),
|
||||
post_create_command: None,
|
||||
post_start_command: None,
|
||||
};
|
||||
|
||||
let config = DevContainer::try_from((schema, devcontainer_path)).unwrap();
|
||||
assert_eq!(config.container_file_path, containerfile_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falls_back_to_dockerfile_before_containerfile() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let devcontainer_path = dir.path().join("devcontainer.json");
|
||||
let dockerfile_path = dir.path().join("Dockerfile");
|
||||
fs::write(&dockerfile_path, "FROM alpine\n").unwrap();
|
||||
fs::write(dir.path().join("Containerfile"), "FROM busybox\n").unwrap();
|
||||
|
||||
let schema = DevContainerSchema {
|
||||
name: None,
|
||||
build: DevContainerBuildSchema {
|
||||
dockerfile: None,
|
||||
args: HashMap::new(),
|
||||
},
|
||||
workspace_folder: None,
|
||||
container_env: HashMap::new(),
|
||||
post_create_command: None,
|
||||
post_start_command: None,
|
||||
};
|
||||
|
||||
let config = DevContainer::try_from((schema, devcontainer_path)).unwrap();
|
||||
assert_eq!(config.container_file_path, dockerfile_path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "herald-server"
|
||||
version = "1.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
sentry = { workspace = true }
|
||||
sentry-anyhow = { workspace = true }
|
||||
openrouter-rs = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
ring = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
metrics = { workspace = true }
|
||||
metrics-exporter-prometheus = { workspace = true }
|
||||
devcontainer-rs = { path = "../devcontainer-rs" }
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Reference in new issue
Block a user