Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: CI

on:
pull_request:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
name: Rust and dashboard
runs-on: ubuntu-24.04
timeout-minutes: 30
env:
CARGO_TERM_COLOR: always
CARGO_BUILD_JOBS: 2
CARGO_PROFILE_DEV_DEBUG: 0
CARGO_PROFILE_TEST_DEBUG: 0
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '22'
cache: npm
cache-dependency-path: web/dashboard/package-lock.json
- name: Install protobuf compiler
run: |
sudo apt-get update
sudo apt-get install --yes protobuf-compiler
- name: Install Rust
run: |
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
rustup default stable
- name: Check Rust formatting
run: cargo fmt --all -- --check
- name: Install dashboard dependencies
working-directory: web/dashboard
run: npm ci
- name: Test dashboard events and state
working-directory: web/dashboard
run: npm test
- name: Build dashboard
working-directory: web/dashboard
run: npm run build
- name: Check embedded dashboard assets
run: |
git diff --exit-code -- services/dashboard/assets/react
test -z "$(git ls-files --others --exclude-standard services/dashboard/assets/react)"
- name: Check independent service builds
run: |
for package in devcloud-s3 devcloud-gcs devcloud-bigquery devcloud-dynamodb devcloud-redshift devcloud-applicationautoscaling; do
cargo check --locked -p "$package"
done
- name: Run Rust tests
run: cargo test --locked --workspace --no-fail-fast
- name: Build CLI for browser test
run: cargo build --locked -p devcloud-orchestrator
- name: Install browser
working-directory: web/dashboard
run: npx playwright install --with-deps chromium
- name: Test dashboard in Chromium
working-directory: web/dashboard
run: npm run test:browser
- name: Upload browser verification
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: dashboard-browser
path: target/dashboard-browser
if-no-files-found: ignore
retention-days: 30
- name: Check Rust lints
run: cargo clippy --locked --workspace --all-targets

audit:
name: Dependency audit
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '22'
cache: npm
cache-dependency-path: web/dashboard/package-lock.json
- name: Audit dashboard dependencies
working-directory: web/dashboard
run: |
npm ci --ignore-scripts
npm audit
- name: Audit Rust dependencies
env:
AUDIT_VERSION: '0.22.2'
AUDIT_SHA256: 7fb9497f8594b389e5fce5ef9b92db08432996895b2e0c5a0167a69ed445c428
run: |
archive="$RUNNER_TEMP/cargo-audit.tgz"
curl --fail --silent --show-error --location \
"https://github.com/rustsec/rustsec/releases/download/cargo-audit/v$AUDIT_VERSION/cargo-audit-x86_64-unknown-linux-musl-v$AUDIT_VERSION.tgz" \
--output "$archive"
printf '%s %s\n' "$AUDIT_SHA256" "$archive" | sha256sum --check
tar -xzf "$archive" -C "$RUNNER_TEMP"
"$RUNNER_TEMP/cargo-audit-x86_64-unknown-linux-musl-v$AUDIT_VERSION/cargo-audit" audit
16 changes: 8 additions & 8 deletions Cargo.lock

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

47 changes: 43 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,9 @@ Legend:
| Dashboard inbox | Yes | Inspect messages and raw source. |
| Delete messages | Yes | Single-message and clear-all paths are available through dashboard API. |
| Outbound relay | No | devcloud is an inbox emulator, not an SMTP relay. |
| SMTP AUTH | No | Default `auth.smtp.mode` is `off`. |
| SMTP AUTH | Partial | `PLAIN` and `LOGIN` are available when `auth.smtp.mode` is not `off`; strict mode validates supplied credentials. `MAIL FROM` before AUTH remains accepted. The default mode is `off`. |
| HTTP API authentication | Partial | Strict mode protects the introspection/control API with the configured SMTP Basic credentials; the dashboard forwards those credentials for reads and mutations. |
| Browser guards | Yes | Direct introspection/control HTTP endpoints reject public DNS Host names and cross-site mutations, using the Dashboard API host allowlist. SDK/CLI requests without Origin remain supported. |
| TLS / STARTTLS | No | Local plaintext development endpoint only. |
| IMAP / POP3 | No | Not implemented. |

Expand Down Expand Up @@ -347,6 +349,8 @@ Legend:
| IAM/ACLs, retention, lifecycle | No | Not implemented. |
| Pub/Sub notifications, signed URLs | No | Not implemented. |

Completed resumable uploads retain a small metadata receipt and notification marker in the shared object-store root until the workspace is reset; these contain no uploaded body and let retries return the original result after restarts, overwrites, or deletions.

### BigQuery-Compatible REST API

| Feature | Status | Notes |
Expand Down Expand Up @@ -441,12 +445,14 @@ Pub/Sub dashboard actions are available under `/dashboard/pubsub`:
| Feature | Status | Notes |
| --- | --- | --- |
| Redis wire endpoint | Yes | Uses a real `redis-server` child process in managed mode; devcloud does not re-implement RESP. |
| External Redis mode | Yes | `services.redis.mode: external` validates `externalUrl` with `PING` and exposes the same dashboard surface. |
| External Redis mode | Yes | `services.redis.mode: external` accepts `redis://[:password@]host[:port][/db]`, validates `AUTH`/`SELECT`/`PING`, and uses that target for dashboard commands. Credentials are omitted from the displayed endpoint. TLS and non-default ACL users are not supported. |
| Managed persistence | Yes | Runtime data is kept under `.devcloud/data/redis` by default. |
| String/hash/list/set/zset inspection | Partial | `/dashboard/redis` lists keys with SCAN and shows type-specific previews. |
| Command runner | Partial | Dashboard commands are restricted to the Redis allowlist in `docs/design-redis-compat.md`. |
| Destructive actions | Partial | Key delete, expire, and guarded `FLUSHDB` are supported; `FLUSHALL` and unsafe commands are rejected. |
| Redis AUTH | Partial | Relaxed mode does not set `requirepass`; strict mode passes the configured password to managed Redis. |
| HTTP API authentication | Partial | Strict mode protects the Redis control API with the configured Redis Basic credentials; the dashboard forwards those credentials for reads and mutations. |
| Browser guards | Yes | Direct introspection/control HTTP endpoints reject public DNS Host names and cross-site mutations, using the Dashboard API host allowlist. SDK/CLI requests without Origin remain supported. |
| Cluster, Sentinel, modules, RedisJSON, RediSearch | No | Out of local compatibility scope. |

### AWS Lambda-Compatible API
Expand Down Expand Up @@ -501,11 +507,44 @@ Pub/Sub dashboard actions are available under `/dashboard/pubsub`:

### Local build and test

Install a current stable Rust toolchain, Node.js 22 or later, and the Protocol
Buffers compiler (`protoc`, including its standard `.proto` files). Pub/Sub's
build script uses `protoc`; on Debian/Ubuntu, install `protobuf-compiler`.

```bash
cd web/dashboard
npm ci
npm test
npm run build
cd ../..
cargo build --locked --workspace
cargo test --locked --workspace
```

The dashboard build writes the assets embedded by the Rust dashboard crate.
Commit the generated bundle with dashboard changes. `npm test` exercises the
event relay frame contract, reconnects, resynchronization, subscription cleanup,
and stale React responses without requiring a browser.

To exercise the built CLI and dashboard together in Chromium:

```bash
cargo build --workspace
cargo test --workspace
cargo build --locked -p devcloud-orchestrator
cd web/dashboard
npx playwright install --with-deps chromium
npm run test:browser
```

The browser test starts an isolated GCS workspace and verifies that provider
mutations appear through the live event stream, including a snapshot refresh
after a disconnected interval. Its screenshot and result are written to
`target/dashboard-browser/` and uploaded as a CI artifact.

Pull requests run `.github/workflows/ci.yml`: Rust formatting, individual service
builds, workspace tests and Clippy, dashboard tests and bundle consistency,
the Chromium smoke test, and Rust/npm dependency audits. Individual builds catch missing Cargo features that
can otherwise be hidden by workspace-wide feature unification.

Service acceptance gates and E2E scripts are run locally because they require service-specific external tooling (`awscli-local`, `postgres` server binaries, `aws` CLI, etc.).

### Manual acceptance gates (per service)
Expand Down
48 changes: 38 additions & 10 deletions orchestrator/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
use std::collections::HashMap;
use std::fs;
use std::io::{self, BufRead};
use std::path::{PathBuf, MAIN_SEPARATOR};
use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};

// ---------------------------------------------------------------------------
// Config struct types (legacy config.rs lines ~12-274)
Expand Down Expand Up @@ -1723,21 +1723,47 @@ pub fn default_config_yaml(cfg: &Config) -> String {
// InitWorkspace / ResetWorkspace (legacy config.rs lines ~513-637)
// ---------------------------------------------------------------------------

/// legacy `validateStoragePath`: path must be under `.devcloud`.
/// Storage operations must stay inside the workspace's `.devcloud` directory.
/// Check components instead of a string prefix: `PathBuf` does not resolve
/// `..`, and an intermediate symbolic link could redirect a reset elsewhere.
fn validate_storage_path(path: &str) -> io::Result<()> {
let clean = clean_path(path);
let prefix = format!(".devcloud{MAIN_SEPARATOR}");
if clean == ".devcloud" || clean.starts_with(&prefix) {
return Ok(());
let invalid = || {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("storage.path must be under .devcloud without parent traversal: {path}"),
)
};
let mut components = Path::new(path)
.components()
.filter(|component| *component != Component::CurDir);
if components.next() != Some(Component::Normal(".devcloud".as_ref()))
|| components.any(|component| !matches!(component, Component::Normal(_)))
{
return Err(invalid());
}
Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("storage.path must be under .devcloud: {path}"),
))

let mut ancestor = PathBuf::new();
for component in Path::new(path).components() {
ancestor.push(component.as_os_str());
match fs::symlink_metadata(&ancestor) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("storage.path must not follow symbolic links: {path}"),
));
}
Ok(_) => {}
// Descendants cannot exist before this directory is created.
Err(err) if err.kind() == io::ErrorKind::NotFound => break,
Err(err) => return Err(err),
}
}
Ok(())
}

/// legacy `ensureFile`: write `data` only if `path` does not already exist.
fn ensure_file(path: &str, data: &[u8]) -> io::Result<()> {
validate_storage_path(path)?;
match fs::metadata(path) {
Ok(_) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotFound => fs::write(path, data),
Expand All @@ -1746,6 +1772,8 @@ fn ensure_file(path: &str, data: &[u8]) -> io::Result<()> {
}

fn mkdir_all(path: &str, ctx: &str) -> io::Result<()> {
validate_storage_path(path)
.map_err(|err| io::Error::new(err.kind(), format!("{ctx}: {err}")))?;
fs::create_dir_all(path).map_err(|err| io::Error::new(err.kind(), format!("{ctx}: {err}")))
}

Expand Down
Loading