feat(pulumi-esc): add Pulumi ESC provider and lease backend#507
feat(pulumi-esc): add Pulumi ESC provider and lease backend#507NorthIsUp wants to merge 13 commits into
Conversation
Provider fetches individual values from ESC environments via `esc env get <ref> <path> --value string --show-secrets`, with a batch path that opens the environment once and extracts dot-delimited paths from the resolved JSON. Lease backend runs `esc env open --format json` and surfaces entries from `environmentVariables` so ESC-minted dynamic credentials (AWS OIDC, GCP OIDC, etc.) can be consumed through fnox's lease system. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace tokio::process::Command shell-out with direct calls to the
Pulumi ESC REST API (POST /api/esc/environments/{ref}/open, then GET
.../open/{id}). Removes the hard dependency on the `esc` CLI for lease
creation.
Credential resolution now mirrors the esc CLI: config token →
FNOX_PULUMI_ACCESS_TOKEN → PULUMI_ACCESS_TOKEN → $PULUMI_HOME or
~/.pulumi credentials.json. The `current` backend URL from the file is
honored so self-hosted Pulumi Cloud works without extra config.
check_prerequisites runs the real resolver instead of the broken
"must have PULUMI_ACCESS_TOKEN" check.
Add an `interpolate` option (char, default none) on the pulumi-esc
lease backend. When set (e.g. `interpolate = "%"`), single-pass
`<sigil>{path}` substitution runs on each surfaced env var, resolving
against the response's properties tree. Missing references are a hard
error. No recursion — `%{foo.%{bar}}` parses as `%{foo.%{bar}` and
errors on lookup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the shared ESC REST client into `src/pulumi_esc_api.rs`
(credential discovery, env_ref builder, `EscClient` with `open`/`read`,
and a `properties`-tree `lookup` walker). Both the lease backend and
the secret provider now drive this module instead of shelling out to
the `esc` CLI.
Provider: fetches individual values via POST /open + GET /open/{id},
extracting dot-delimited paths locally (both single-get and batch).
Drops `tokio::process::Command`, the `classify_cli_error` heuristics,
and the separate `walk`/`extract_path`/`value_to_string` helpers —
the shared `lookup` walker covers all paths the CLI JSON format did.
Lease backend: same refactor, plus it now re-uses the shared auth
resolver for `check_prerequisites`. `resolve_refs` and `extract_value`
stay local since they're lease-specific.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docs: both pulumi-esc.md files said the `esc` CLI was a prerequisite
and described the old CLI shell-out (`esc env open --format json`,
`esc env get ... --show-secrets`). Rewritten to reflect the native
HTTP path. Kept `esc login` as one of the auth options since it
populates `~/.pulumi/credentials.json`, which fnox still reads.
- `EscClient::open` now sends `Content-Type: application/json` and an
explicit `{}` body on the POST so middleware/proxies that enforce
a media type don't reject with 415.
- `pulumi_esc_api::lookup` now early-returns None for empty paths so
the intent is explicit rather than relying on `cur.get("")`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses PR jdx#429 review feedback: - Revert dev version bump in Cargo.toml / fnox.usage.kdl (jdx flagged as unintentional). The fork-only dev tag is no longer needed. - Switch std::env::var → crate::env::var for PULUMI_HOME and PULUMI_BACKEND_URL to match the rest of the codebase (gemini-code-assist). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulumi Cloud org/project/env names are typically [a-zA-Z0-9-], but the env ref is interpolated directly into the request path. Encoding each segment is cheap insurance against a corrupted URL if a name ever contains `/`, `?`, `#`, or whitespace. Addresses Greptile P2 on PR jdx#429. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The provider metadata still pointed at the esc CLI for auth-prompting and setup — contradicting the rest of the PR where fnox talks to the Pulumi Cloud REST API directly. Drop auth_command (no CLI to invoke) and rewrite setup_instructions to describe the token / credentials.json paths. Addresses Greptile P1 on PR jdx#429. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Move `## Authentication` to `### Authentication` under `## Configuration` - Add missing `## Usage` section - Rename `## Leases (dynamic credentials)` to `## Leases` - Add `### GitHub Actions` subsection under `## CI/CD Example` https://claude.ai/code/session_016UhCuqGUPhdmZ5sYXe3Y15
…ction Module lives in crates/fnox-core/src/pulumi_esc_api.rs after the fnox-core crate refactor. https://claude.ai/code/session_016UhCuqGUPhdmZ5sYXe3Y15
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eview
The percent-encoding fix for env-ref path segments was applied to the
stale `src/pulumi_esc_api.rs` before the crate extraction and silently
lost when that file was deleted. `build_env_ref` shipped unencoded,
interpolating org/project/env straight into the request path. Restore
per-segment `urlencoding::encode` (the canonical idiom, as in
passwordstate) and add a test.
Quality cleanups from a code-quality review:
- Collapse the duplicate `{value, trace}` scalar coercion: the lease
backend's `extract_value` and the api's inline match are now one
shared `pulumi_esc_api::coerce_scalar`; `lookup` reuses it.
- Replace the dishonest `(String, String)` auth tuple with a named
`EscAuth { base, token }` struct.
- Add `EscClient::{api_err, invalid_err}` helpers to remove the repeated
provider/url error plumbing in `open`/`read`/`http_error`.
- Use `PROVIDER_NAME` instead of bare "Pulumi ESC" literals in the
lease backend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for Pulumi ESC as both a secret provider and a lease backend, allowing fnox to retrieve secrets and dynamic credentials directly from Pulumi ESC environments via the Pulumi Cloud REST API. The feedback recommends using the custom env::var wrapper instead of std::env::var in pulumi_esc_api.rs to maintain consistent environment variable resolution and support test mocking. Additionally, it suggests replacing the deprecated chrono::Duration::seconds with chrono::Duration::from_std in the lease backend implementation.
| let expires_at = | ||
| Some(chrono::Utc::now() + chrono::Duration::seconds(duration.as_secs() as i64)); |
There was a problem hiding this comment.
Use chrono::Duration::from_std to convert the std::time::Duration to a chrono::Duration. This avoids the deprecated chrono::Duration::seconds function and safely handles sub-second precision.
| let expires_at = | |
| Some(chrono::Utc::now() + chrono::Duration::seconds(duration.as_secs() as i64)); | |
| let expires_at = | |
| Some(chrono::Utc::now() + chrono::Duration::from_std(duration).unwrap()); |
There was a problem hiding this comment.
Holding off on this one. chrono::Duration::seconds isn't deprecated in our pinned chrono 0.4 (build emits no deprecation warning), lease durations are whole-second and bounded (max 3600s), and from_std(duration).unwrap() would introduce a panic path for no functional gain here. Keeping the current form.
This reply was generated by Claude Code.
Greptile SummaryAdds Pulumi ESC as both a secret provider and a lease backend, using native HTTP against the Pulumi Cloud REST API (no
Confidence Score: 4/5Safe to merge after fixing check_prerequisites; actual lease creation and secret resolution are unaffected by the bug. The lease backend's check_prerequisites() ignores the config-supplied token and only inspects env vars and credentials.json. A user who configures token in TOML but has neither a PULUMI_ACCESS_TOKEN env var nor a ~/.pulumi/credentials.json will be told prerequisites are not met even though their configuration is valid — the actual create_lease path handles the config token correctly via resolve_auth_for. All other logic is solid and well-tested. crates/fnox-core/src/lease_backends/pulumi_esc.rs and the PulumiEsc arm in crates/fnox-core/src/lease_backends/mod.rs Important Files Changed
Reviews (2): Last reviewed commit: "chore(review): route PULUMI_HOME/PULUMI_..." | Re-trigger Greptile |
Address gemini-code-assist and greptile review on jdx#507: use the project's `env::var` wrapper (re-exported, test-mockable) instead of `std::env::var` for PULUMI_HOME and PULUMI_BACKEND_URL, matching the convention used by the token lookups and every other provider. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks @gemini-code-assist and @greptile-apps — addressed in d06b57e:
This comment was generated by Claude Code. |
|
Thanks for the update, @NorthIsUp. The switch to |
|
Thanks for the follow-up. The On Tip: You can customize Greptile's behavior for this repo with |
Summary
Add Pulumi ESC (Environments, Secrets, Configuration) support to fnox as both a secret provider and a lease backend, with native HTTP throughout — no esc CLI dependency.
crates/fnox-core/src/providers/pulumi_esc.rs): fetch individual values from an ESC environment via dot-path (e.g.anthropic.api_key→properties.anthropic.value.api_key.value). Single-get and batch (one env open, multiple local extractions) share the same code path.crates/fnox-core/src/lease_backends/pulumi_esc.rs): mint short-lived credentials by opening an ESC environment and surfacing entries from itsenvironmentVariablesblock — lets ESC-minted dynamic credentials (AWS OIDC, GCP OIDC, Vault, etc.) flow into fnox's lease ledger.crates/fnox-core/src/pulumi_esc_api.rs): both call sites use oneEscClientwith one credential resolver, onePOST /open+GET /open/{id}pair, and oneproperties-tree walker.Config
fnox.toml:Credential resolution (matches esc CLI)
tokenin TOML configFNOX_PULUMI_ACCESS_TOKENPULUMI_ACCESS_TOKEN$PULUMI_HOME/credentials.json~/.pulumi/credentials.jsonThe file's
currentfield dictates the API base URL, so self-hosted Pulumi Cloud works without extra config. The env-var path honorsPULUMI_BACKEND_URL.interpolateoption (lease backend)Pulumi ESC allows composition + importing of secret environments, but it does early binding with variables. The interpolation option allows late binding.
When set to a single-char sigil (e.g.
"%"), single-pass<sigil>{path}substitution runs on each surfaced env var.ANTHROPIC_API_KEY = "%{anthropic.api_key}"in an ESC env'senvironmentVariablesblock resolves to the real token.Note: missing refs are a hard error. The feature is single-pass only —
%{foo.%{bar}}is not recursively resolved and will error.Notes
mise run lint(clippy + prettier) andmise run renderare clean.🤖 Generated with Claude Code