12 KiB
Roadmap: HTTP Upload API
Purpose
Add an HTTP upload API that lets producer applications push complete source
bundles into distributor.
The current application already supports local, SSH/SFTP, and S3-compatible
source and destination backends. Those source backends are pull-oriented:
distributor opens configured storage, discovers manifest.json, validates
bundles, and fans out selected outputs to configured destinations.
http_upload is different because it is push-oriented. A producer sends one
complete bundle to distributor, and distributor stages that upload before
running normal validation and fan-out. The HTTP layer should be an ingestion
layer over the existing app, bundle, storage, publish, transform, link, state,
and notification behavior.
Current Implementation Grounding
Implemented behavior already provides the core pieces this feature should reuse:
- source bundle validation through the bundle package;
- local, SSH/SFTP, and S3-compatible storage backends;
- destination fan-out through publish planning and execution;
- single-pipeline app execution through the app layer;
- structured run reports and JSON-capable CLI output;
- secrets-directory environment resolution for credential material.
The HTTP implementation should not duplicate bundle validation or publication logic. Once an upload is staged, it should proceed through the same validation and fan-out behavior as any other source bundle.
Accepted Direction
Add http_upload as a source backend option for configured pipelines.
An http_upload source is not a normal durable storage backend. It represents
an HTTP ingestion endpoint that receives an uploaded bundle, writes it into
pipeline-local staging storage, validates it, and then dispatches the existing
pipeline fan-out flow.
Accepted behavior:
- producer applications upload a compliant source bundle manifest and all referenced files;
- uploads are asynchronous;
- each accepted upload receives a generated run id and an initial
acceptedstatus; - clients can query run status later by run id;
- authentication uses a static token associated with the selected
http_uploadpipeline; - upload requests send that token with
Authorization: Bearer <token>; - tokens may be supplied through
secrets.directoryusing the existing internal environment resolver; - each
http_uploadpipeline has a configurable upload staging directory; - default staging root is
/var/spool/distributor; - default pipeline staging directory is
/var/spool/distributor/<pipeline_id>; - default maximum upload size is 20 MB;
- upload archives may be uncompressed tar or gzip-compressed tar;
- the server must prevent multiple simultaneous active runs of the same pipeline;
- the server uses an internal bounded queue and a global
max_concurrencysetting for accepted uploads; - completed status records and staged run directories use time-based retention;
- every accepted upload is a new run with a
distributor-generated run id.
Configuration Shape
Add http_upload as a source backend only. It should not be valid as a
destination backend.
Add top-level HTTP server configuration for cross-pipeline server behavior:
server:
http:
bind: 127.0.0.1:8080
staging_root: /var/spool/distributor
max_upload_size: 20MB
queue_size: 16
max_concurrency: 1
retention: 24h
Defaults:
bind:127.0.0.1:8080;staging_root:/var/spool/distributor;max_upload_size:20MB;queue_size:16;max_concurrency:1;retention:24h.
Pipeline shape:
pipelines:
- id: weather-daily
source:
backend: http_upload
token_env: WEATHER_DAILY_UPLOAD_TOKEN
staging_path: /var/spool/distributor/weather-daily
max_upload_size: 20MB
destinations:
- id: archive
backend: s3
...
token_env is required for the first implementation. Literal token values in
YAML are not supported. Real environment variables or secrets.directory files
provide the token without putting secret values in config.
If staging_path is omitted, default it to:
/var/spool/distributor/<pipeline_id>
If source-level max_upload_size is omitted, use the top-level
server.http.max_upload_size value.
Server-level queue configuration is separate from per-pipeline source configuration. Per-pipeline fields may override staging path and upload size; bind address, queue size, concurrency, and retention are server-level settings.
Upload Model
The uploaded request should contain one complete source bundle.
Use archive upload rather than multipart fields for each file. distributor
should extract the archive into a per-run staging directory under the pipeline
staging path, then validate the extracted bundle.
The first implementation supports:
- uncompressed tar;
- gzip-compressed tar.
The server should accept uncompressed tar as application/x-tar. It should
accept gzip-compressed tar as application/gzip or application/x-gzip.
Archive extraction must be conservative:
- reject absolute paths;
- reject path traversal and backslash paths;
- reject symlinks, hardlinks, devices, sockets, and other special entries;
- require exactly one root-level
manifest.json; - require all manifest-listed files to be present as regular files;
- reject extra nested manifests;
- enforce upload size limits before extraction;
- enforce extracted size and file-count limits after extraction;
- clean up failed extraction directories.
After extraction, validation should use the existing source bundle validation contract. A bundle that fails manifest, path, size, digest, or file validation must not be published.
Async Run Flow
HTTP upload processing should be asynchronous:
- Authenticate the request token and map it to exactly one
http_uploadpipeline. - Admit or reject the request according to queue capacity and per-pipeline active-run rules.
- Create a run id and per-run staging directory.
- Record initial in-memory run status as
accepted. - Return an acceptance response without waiting for fan-out to complete.
- In a worker, extract the archive, validate the staged bundle, and run the pipeline fan-out using the staged bundle as the effective source.
- Record final success or failure status.
Initial acceptance response shape:
{
"run_id": "weather-daily.20260603T120000Z.ab12cd34",
"status": "accepted"
}
Run ids should be generated by distributor. Use the pipeline id, a
filesystem-safe UTC timestamp, and a short random suffix to avoid collisions.
Suggested status values:
accepted;queued;running;succeeded;failed;expired.
Status records should include run id, pipeline id, status, timestamps, and the completed run report or error details when available. Status records are kept in memory for the first implementation and expire after the configured retention period.
Every accepted upload is treated as a new run. The first implementation does not accept producer-supplied idempotency keys.
Queue And Concurrency
The server must not run more than one upload-triggered execution for the same pipeline at the same time.
Use two controls:
- a global worker limit, configured as
max_concurrency; - a bounded admission queue, configured as
queue_size; - a per-pipeline single-active-run guard.
If a run for the same pipeline is already active, later accepted uploads for that pipeline should wait in queue rather than starting concurrently.
If the queue is full, the upload request should fail before consuming and staging the request body. The server must not allow unbounded memory or disk growth.
Authentication
Each http_upload pipeline has one static upload token.
Upload requests send the token in an HTTP bearer header:
Authorization: Bearer <token>
Authentication maps the incoming bearer token to exactly one configured pipeline. If no pipeline matches, the request fails. If more than one pipeline resolves to the same token, config validation should fail before the server starts.
Secret values must never be logged, returned in responses, or included in run status.
HTTP Server Boundary
Add a server mode rather than trying to make run poll an HTTP source.
The likely CLI shape is:
distributor serve --config <path>
The HTTP API should default to a private bind address. Operators that need
public access, TLS termination, or mTLS should place distributor behind a
reverse proxy or private network boundary unless a later roadmap explicitly
adds in-app TLS.
The server should expose:
POST /upload;GET /runs/<run_id>;GET /healthz.
GET /healthz should return success after the server has loaded and validated
configuration and is ready to accept requests.
Relationship To Existing Architecture
http_upload should reuse existing code paths after staging:
- upload staging should produce a local staged bundle tree;
- staged bundle validation should use the existing bundle validation contract;
- fan-out should use the app-layer single-pipeline run behavior where possible;
- destination handling should remain backend-agnostic;
- publish, transform, link, state, and notification behavior should not know that the source arrived over HTTP.
If the current app-layer single-pipeline runner assumes it can open and walk the
configured source backend, the HTTP implementation should add a narrow app-layer
entry point for "run this pipeline using this already-staged source backend"
rather than bending http_upload into a fake durable storage backend.
Non-Goals
The first HTTP upload implementation should not add:
- destination-side HTTP upload;
- browser UI;
- producer execution;
- source manifest schema changes;
- warning-only digest mismatch behavior;
- durable database-backed queueing;
- producer-supplied idempotency keys;
- zstd archive support;
- URL-token authentication;
- public network exposure defaults;
- in-app TLS;
- general-purpose workflow orchestration.
Testing Expectations
Suggested coverage:
- config validation accepts
http_uploadsources and rejectshttp_uploaddestinations; - default staging path becomes
/var/spool/distributor/<pipeline_id>; - token env references resolve through real environment and
secrets.directory; - duplicate token values across pipelines fail validation;
- upload size limit defaults to 20 MB and is enforced;
- archive extraction rejects unsafe paths, symlinks, hardlinks, devices, missing manifest, missing manifest-listed files, and nested manifests;
- valid uploaded bundles validate through the existing bundle contract;
- upload admission returns
acceptedand a generated run id; - status lookup reports queued, running, succeeded, failed, and expired states;
- per-pipeline runs do not execute concurrently;
- global
max_concurrencyis honored; - full queues reject uploads before request-body staging;
GET /runs/<run_id>returns in-memory status records until retention expiry;- failed extraction and failed runs clean up or retain staging data according to the configured retention policy;
- secret values never appear in logs, responses, or status records;
- normal local, SSH/SFTP, and S3 source behavior remains unchanged.
Documentation Updates After Implementation
- Update
docs/config.mdwithhttp_uploadsource fields and defaults. - Update
docs/cli.mdwithservesyntax and HTTP behavior. - Update
docs/operations.mdwith upload, queue, status, and staging workflows. - Update
docs/troubleshooting.mdfor authentication, archive extraction, validation, queue, and fan-out failures. - Add examples only if they are secret-free and safe to run locally.
- Update internal docs for any new app, HTTP, queue, or ingestion packages.
Keep this roadmap under docs/roadmap/ until implemented.
Future Work
The first implementation intentionally defers:
- URL-token authentication;
- zstd-compressed archive support;
- durable status persistence across process restarts;
- database-backed queueing;
- producer-supplied idempotency keys;
- run listing, cancellation, or retry endpoints;
- in-app TLS;
- public network exposure defaults;
- browser UI.
These items should remain out of current-behavior docs until a later roadmap selects and specifies them.