Expand description
HttpEnvironmentStore — remote HTTP-backed implementation of
EnvironmentMutations.
Talks to a future greentic-operator-store-server (PR-4) over the
A8 HTTP contract specified in greentic_deploy_spec::remote.
JSON over the wire, reqwest::blocking::Client for transport so the
sync EnvironmentMutations trait stays sync (no Tokio runtime needed
at call sites — see project memory project_next_gen_deployment_phase_b
for the block_in_place panic precedent that rules out async-in-sync).
§Route table
Every mutation maps to a single HTTP endpoint. The server (PR-4) mirrors this table.
| Trait method | Method | Path |
|---|---|---|
create_environment | POST | /environments |
update_environment | PATCH | /environments/{env_id} |
migrate_merge_bindings | POST | /environments/{env_id}/migrate-bindings |
stage_revision | POST | /environments/{env_id}/revisions |
warm_revision | POST | /environments/{env_id}/revisions/{rid}/warm |
drain_revision | POST | /environments/{env_id}/revisions/{rid}/drain |
archive_revision | POST | /environments/{env_id}/revisions/{rid}/archive |
add_bundle | POST | /environments/{env_id}/bundles |
update_bundle | PATCH | /environments/{env_id}/bundles/{deployment_id} |
remove_bundle | DELETE | /environments/{env_id}/bundles/{deployment_id} |
add_pack_binding | POST | /environments/{env_id}/packs |
update_pack_binding | PATCH | /environments/{env_id}/packs/{slot} |
remove_pack_binding | DELETE | /environments/{env_id}/packs/{slot} |
rollback_pack_binding | POST | /environments/{env_id}/packs/{slot}/rollback |
add_extension_binding | POST | /environments/{env_id}/extensions |
update_extension_binding | PATCH | /environments/{env_id}/extensions |
remove_extension_binding | DELETE | /environments/{env_id}/extensions |
rollback_extension_binding | POST | /environments/{env_id}/extensions/rollback |
set_traffic_split | POST | /environments/{env_id}/traffic |
rollback_traffic_split | POST | /environments/{env_id}/traffic/rollback |
add_messaging_endpoint | POST | /environments/{env_id}/messaging |
link_messaging_bundle | POST | /environments/{env_id}/messaging/{eid}/link |
unlink_messaging_bundle | POST | /environments/{env_id}/messaging/{eid}/unlink |
set_messaging_welcome_flow | POST | /environments/{env_id}/messaging/{eid}/welcome-flow |
remove_messaging_endpoint | DELETE | /environments/{env_id}/messaging/{eid} |
rotate_messaging_webhook_secret | POST | /environments/{env_id}/messaging/{eid}/rotate-secret |
bootstrap_trust_root | POST | /environments/{env_id}/trust-root/bootstrap |
seed_trust_root_if_absent | POST | /environments/{env_id}/trust-root/seed |
add_trusted_key | POST | /environments/{env_id}/trust-root/keys |
remove_trusted_key | DELETE | /environments/{env_id}/trust-root/keys/{key_id} |
load_environment | GET | /environments/{env_id} |
load_environment is the one READ verb (no idempotency key, no audit
envelope) — the remote dispatch uses it to evaluate client-side
preconditions such as the warm health-gate’s expected lifecycle.
The backup/restore group (A8 #5, PR-4.4) is server-only — LocalFsStore
has no implementation, so these are inherent methods on
HttpEnvironmentStore, not EnvironmentMutations verbs:
| Inherent method | Method | Path |
|---|---|---|
create_backup | POST | /environments/{env_id}/backups |
list_backups | GET | /environments/{env_id}/backups |
delete_backup | DELETE | /environments/{env_id}/backups/{backup_id} |
restore | POST | /environments/{env_id}/restore |
§Headers
Content-Type: application/json/Accept: application/jsonon every request.Authorization: Bearer <token>whenAuthMethod::Bearer.Idempotency-Key: <ulid>when the payload carries anIdempotencyKey.
§ETag / CAS
The store does ETag-chained optimistic concurrency entirely at the
transport layer — no change to the EnvironmentMutations trait or any
CLI call site. Per environment, it remembers the last strong ETag it
observed (from the [MutationEnvelope] of every successful mutation, and
from load_environment’s
response) in [Self::cached_etag], and replays it as a quoted If-Match
header on the next mutation against that env. The server (A8 §1) then
rejects the write with 412 if any other writer advanced the environment
since that ETag. The cache is keyed by env because one store represents a
remote endpoint, not a single env.
This catches the two windows a per-invocation CLI actually exposes:
- read-then-decide-then-write within one command (e.g.
revisions warmreads the lifecycle, runs health checks, then writes) — the write is pinned to the ETag the decision was based on; - multi-write commands (e.g.
op env apply) — each write is pinned to the ETag the previous write returned, so a concurrent writer slipping between two of our writes is caught.
A single-write command on a fresh store has no cached ETag, so it sends
no If-Match; the server’s own load-pinned generation (a torn-write
guard) still applies. Cross-invocation lost-update protection (one
operator overwriting another’s separately-issued command) needs an
explicit caller-supplied expectation and is a separate, opt-in follow-up.
A 412/428 clears the cache so a stale ETag is never replayed.
LocalFsStore needs none of this — its flock serialises writers — so
CAS lives only here, not on the shared trait.
§Error mapping
Transport errors (connection refused, timeout, TLS handshake) map to
StoreError::Conflict("transport: ..."). A dedicated
StoreError::Transport variant would be cleaner but adding a new enum
variant cascades into every match site — follow-up.
§Follow-ups
- Explicit cross-invocation CAS (caller-supplied
--expect-generation) StoreError::TransportvariantAuthMethod::Mtlsfor production (mTLS)- PR-3c wires dispatch between
LocalFsStoreandHttpEnvironmentStore
Structs§
- Http
Environment Store - Remote HTTP-backed implementation of
EnvironmentMutations.
Enums§
- Auth
Method - How the client authenticates to the remote store server.
- Construction
Error - Errors that can occur when constructing an
HttpEnvironmentStore.