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
6 changes: 4 additions & 2 deletions .github/workflows/auto-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ jobs:
fetch-depth: 0
- name: Check for version bump
id: version
env:
BEFORE_SHA: ${{ github.event.before }}
run: |
CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
if [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
PREVIOUS="$CURRENT"
else
PREVIOUS=$(git show ${{ github.event.before }}:Cargo.toml | grep '^version' | head -1 | sed 's/.*"\(.*\)"/\1/')
PREVIOUS=$(git show "$BEFORE_SHA":Cargo.toml | grep '^version' | head -1 | sed 's/.*"\(.*\)"/\1/')
fi
echo "current=$CURRENT" >> "$GITHUB_OUTPUT"
echo "previous=$PREVIOUS" >> "$GITHUB_OUTPUT"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --check
- run: cargo clippy -- -D warnings
- run: cargo clippy --all-targets -- -D warnings
test:
runs-on: ubuntu-latest
steps:
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ jobs:
- run: cargo test --all
- name: Publish to crates.io
run: |
cargo publish 2>&1 || {
if cargo search restium --limit 1 | grep -q "$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')"; then
echo "::warning::Version already published to crates.io — skipping"
else
echo "::error::cargo publish failed"
exit 1
fi
}
OUTPUT=$(cargo publish 2>&1) && exit 0
if echo "$OUTPUT" | grep -q "already uploaded"; then
echo "::warning::Version already published to crates.io — skipping"
else
echo "$OUTPUT"
echo "::error::cargo publish failed"
exit 1
fi
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- uses: docker/setup-qemu-action@v3
Expand Down
126 changes: 126 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- CLI skeleton with `reconcile` and `validate` subcommands
- Global flags: `--json`, `--insecure-tls`
- `RESTIUM_*` environment variable support for all flags
- Exit code convention: 0 (success), 1 (runtime error), 2 (input/validation error)
- Structured logging with JSON (`--json`) and human-readable text modes
- Automatic secret redaction for sensitive key-value pairs (authorization, token, password, api_key, client_secret, etc.)
- URL query parameter redaction for secrets in logged URLs
- YAML spec file parsing with `global` settings and `resources` list
- Global settings inheritance: base_url, default_headers, auth (with per-resource override)
- Structured payloads (nested objects, arrays) preserved through YAML-to-JSON
- `depends_on` field for explicit resource ordering
- `deny_unknown_fields` on spec types for typo detection
- AuthConfig type definitions (bearer, basic, api_key, oidc, mtls) — type stubs for Epic 2
- `validate` and `reconcile` commands now parse and report spec file contents
- Spec validation: reference checking (`${resource.output.field}`), duplicate resource name detection, circular dependency detection
- `validate` command reports ALL errors (not just the first), exits with code 2 on validation failure
- Dependency graph module (`src/graph/mod.rs`) with `petgraph`-based topological sort and cycle detection
- `DependencyGraph::build()` merges explicit `depends_on` and implicit `${resource.output.field}` references into a single DAG
- `DependencyGraph::topological_sort()` returns execution order or cycle path error
- Reconcile module (`src/reconcile/`) with state discovery, diff computation, and action determination
- `ResourceAction` enum: Create, Update, Skip, Delete
- Key-order-independent JSON comparison (`json_equal`) for idempotent reconciliation
- State discovery via GET with 404 handling (resource does not exist → Create)
- Resource execution module (`src/reconcile/execute.rs`) for create/update/skip operations
- Actionable error messages with resource name, HTTP method, endpoint, status code, and hints
- Error hints for common HTTP status codes (401, 403, 404, 409, 422, 429, 5xx)
- Update operations auto-switch from POST to PUT when resource method is POST
- Explicit resource deletion via `action: delete` with DELETE HTTP request
- Delete of absent resource (404) logged as "already absent" and skipped
- Reconcile command orchestration: parse → graph → sort → for each resource: resolve refs → discover state → compute diff → execute → extract outputs
- Reconciliation summary logged on completion (created, updated, deleted, skipped, failed counts)
- Delete resources processed in reverse topological order (dependents deleted before dependencies)
- Exit code 1 on reconciliation failure, exit code 0 on success
- Reference module (`src/reference/mod.rs`) for output extraction and `${resource.output.field}` resolution
- `OutputStore` type for storing extracted outputs across resources
- `extract_outputs()` extracts fields from API responses (string, number, bool, null, complex)
- `resolve_references()` recursively substitutes template references in JSON payloads
- `resolve_string()` resolves references in individual strings (endpoints, read_endpoints)
- HTTP client wrapper (`HttpClient`) with configurable TLS via `ureq` + `rustls`
- TLS certificate verification enabled by default (webpki roots)
- `--insecure-tls` flag skips certificate verification (explicit opt-in, NFR3)
- Custom CA bundle support via `ca_bundle` field in global config
- `AuthProvider` trait for pluggable authentication strategies
- Bearer token authentication via `token_env` environment variable
- Basic auth (username/password) via environment variables with base64 encoding
- Auth provider factory (`create_auth_provider`) for config-driven provider selection
- API key authentication via header or query parameter mode
- OIDC/OAuth2 client credentials authentication (token fetched at startup via POST to token endpoint)
- mTLS client certificate authentication via `client_cert_path` and `client_key_path`
- mTLS works in combination with custom CA bundles
- Per-resource auth override: resources can specify their own `auth` config, overriding global auth
- Shared `read_env_credential` helper validates env vars are set, non-empty, and UTF-8 across all auth providers
- `--sidecar` flag (and `RESTIUM_SIDECAR` env var) to keep the process alive after reconciliation completes — for K8s sidecar container deployments where the container must not exit
- Distroless Docker image (`FROM scratch`) with multi-stage build — 3.5MB, zero CVEs, non-root user
- Multi-arch container support (linux/amd64, linux/arm64) via Docker buildx with QEMU in release pipeline
- `make cross` target for local musl cross-compilation via cargo-zigbuild
- `make docker-multiarch` target for local multi-arch Docker builds
- CI pipeline (GitHub Actions): fmt check, clippy with `--all-targets`, test, build on every PR and push to main
- Release pipeline: automated cargo publish + multi-arch GHCR push on version tags, with SBOM and provenance
- Auto-tag workflow: automatically creates git tags when Cargo.toml version changes on main
- Comprehensive README with value proposition, quick start, Netbird example, spec reference, auth docs, security posture, CLI reference, and deployment guide
- Example spec files: `examples/simple.yaml` (minimal), `examples/netbird.yaml` (real-world Netbird bootstrapping)
- Helm chart (`charts/restium/`) for K8s Job deployment with ConfigMap-mounted spec and Secret-based credentials

### Changed

- `validate` command no longer creates an HTTP client (pure syntax/semantic validation)
- Spec file loading uses `SpecFile::load` error directly instead of redundant existence check
- Validation now scans `endpoint` field for template references (previously only `payload` and `read_endpoint`)
- Cycle detection replaced from manual DFS to `petgraph`-based implementation via `DependencyGraph`
- Reference extraction functions (`extract_refs_from_string`, `extract_references_from_value`) made public for reuse by graph module
- `--insecure-tls` and mTLS client certificates are now mutually exclusive (previously mTLS cert was silently dropped)
- Lint target now includes test code (`cargo clippy --all-targets`)
- Test fixture paths use `CARGO_MANIFEST_DIR` for CWD-independent test execution
- `HttpClient::get()` now accepts `auth: Option<&dyn AuthProvider>` for consistency with `send_json()` and `request()`
- `NoVerifier::supported_verify_schemes()` caches signature schemes via `LazyLock` instead of re-instantiating `CryptoProvider` per TLS handshake
- mTLS file validation deferred to `HttpClient::new()` — removed redundant `metadata()` check from `MtlsAuthProvider::new()` to eliminate TOCTOU window
- `OidcAuthProvider::with_token()` marked `#[doc(hidden)]` (testing escape hatch, not public API)
- `auth::mtls` and `auth::oidc` submodules changed from `pub mod` to `mod` (types re-exported via `pub use`)
- OIDC token request now uses the configured `HttpClient` agent (inherits TLS settings: custom CA bundle, `--insecure-tls`)
- `create_auth_provider()` accepts optional `&ureq::Agent` for OIDC TLS configuration
- `HttpClient::agent()` method exposes the underlying ureq Agent for auth provider use
- `DependencyGraph::build()` uses `update_edge` instead of `add_edge` to prevent duplicate edges when explicit and implicit deps overlap
- `MtlsAuthProvider` simplified to unit struct — cert/key paths extracted from `AuthConfig` directly, not stored redundantly on the provider
- `execute_action` returns `ExecuteResult` enum (Performed/AlreadyOk) instead of `Option<Value>` — allows callers to distinguish successful operations from skips/already-absent
- Success log in `execute_mutation` moved after response body parse (previously logged success before JSON parse could fail)
- Non-JSON 2xx responses treated as success with no body (previously hard-failed as invalid JSON)
- Update method auto-switch uses case-insensitive comparison (`eq_ignore_ascii_case("POST")`)
- `error_hint` checks transport errors before HTTP status codes to avoid false matches on URL digits
- Delete 404 ("already absent") now increments `skipped` counter instead of `deleted`
- State discovery GET skipped when resource has no payload (previously wasted a network request)

### Security

- OIDC error messages no longer expose `client_secret` — controlled error formatting instead of raw ureq error propagation
- OIDC error messages now extract OAuth error fields (`error`, `error_description`) from the response body for actionable diagnostics
- Auth providers (Bearer, Basic, ApiKey, OIDC) now implement custom `Debug` that redacts credentials instead of using `#[derive(Debug)]`
- Empty env var values rejected with actionable error (previously silently sent empty credentials)
- `VarError::NotUnicode` now produces distinct error message instead of misleading "not set"
- API key auth validates `header_name` and `query_param` are non-empty

### Fixed

- `process_delete_resource` now resolves `${resource.output.field}` template references in endpoints (was using raw template strings, causing 404s or wrong-resource deletion)
- `execute_mutation` now propagates JSON parse errors from API responses instead of silently substituting `null`
- Spec validation rejects unknown `action` values (only `delete` is supported; previously any string was silently treated as create/update)
- OIDC `url_encode` renamed to `form_url_encode` and uses `+` for spaces (HTML form encoding per RFC 1866) instead of `%20` (RFC 3986), fixing multi-word scope values on strict token endpoints
- OIDC `token_url` is now validated as non-empty before making the HTTP request
- Redundant "Error:" prefix in spec file not found message (logger already prepends `[ERROR]`)
- `format!` + `push_str` replaced with `write!` to avoid unnecessary allocations in OIDC URL encoding and text log formatting
- Misleading `// Safety:` comment changed to `// Note:` (not an unsafe block)
- JSON log fallback replaced with `expect()` (serialization of `BTreeMap<String, Value::String>` is infallible)
- Unnecessary `body.clone()` removed in `HttpClient::send_json` (reference already satisfies `Serialize`)
- Malformed template references like `${.output.field}` (empty resource name) are now silently ignored instead of producing confusing errors
- Use `sort_unstable()` instead of `sort()` for primitive `&str` slice in cycle detection
Loading
Loading