17 KiB
Distributor Operations
Audience: administrators and operators who run distributor, publish bundles, operate the HTTP upload service, or recover from failed runs.
This document covers operating workflows, storage layout, safety behavior, and recovery. Command syntax lives in CLI, configuration fields live in Configuration, symptom-specific fixes live in Troubleshooting, and external contracts live under Integrations.
Normal Workflow
Validate a producer bundle before publishing:
go run ./cmd/distributor validate examples/source-bundle
Preview a configured run before writing destination content:
go run ./cmd/distributor run --config examples/local-publish.yml --dry-run
Publish after reviewing the preview:
go run ./cmd/distributor run --config examples/local-publish.yml
Use JSON output for automation:
go run ./cmd/distributor run --config examples/fan-out.yml --dry-run --format json
Use configured source diagnostics when the source is defined in YAML and may be local, SSH/SFTP, or S3-compatible storage:
go run ./cmd/distributor validate --config examples/local-publish.yml --pipeline example-source-bundle
go run ./cmd/distributor inspect --config examples/local-publish.yml --pipeline example-source-bundle --format json
Remote examples under examples/ssh-destination.yml and examples/s3-destination.yml are load-tested templates. Edit their endpoint, path, key, bucket, prefix, and credential values for storage you control before running them.
Filesystem And Storage Layout
A source bundle is a directory containing manifest.json and every file listed in that manifest. See Source Bundle Contract. Source discovery walks beneath the configured source backend root and finds bundle directories.
Each destination has its own backend root:
- Local destinations use the configured local
path. - SSH/SFTP destinations use the configured remote
path. - S3-compatible destinations use the configured
bucketplus optionalprefix.
Destination path mapping controls where each source bundle is published beneath the destination root:
preserve_relativepublishes each source bundle at the same source-root-relative path.fixedpublishes one selected source bundle at the destination root.
Fixed destinations select the newest discovered source bundle by manifest created timestamp. If multiple bundles have the same timestamp, the source-root-relative bundle path in ascending order wins.
Published destination bundle paths contain .distributor.json. See Destination State Contract. This file is both the managed sentinel and the destination catalog. It records catalog schema version 4, output owners, source identity for each output, output digests and sizes, timestamps, and optional URL metadata.
manifest.json from the source bundle is not copied as destination state.
Catalog Publish Behavior
distributor plans from the current source bundle, destination workflow, destination storage content, and .distributor.json.
- No valid state and no destination content: publish new outputs.
- No valid state and existing destination content: fail as unmanaged unless
--forceis used. - Valid catalog state with
workflow: additive: write planned outputs and retain unrelated catalog outputs. - Valid catalog state with
workflow: replacement: write planned outputs and remove omitted outputs owned by the selected pipeline and destination. - Planned output path exists in storage but is not recorded in valid catalog state - fail as unmanaged unless
--forceis used. - Invalid destination state or unsupported future state - fail as conflict unless
--forceis used. - Superseded legacy state - publish through the catalog planner and write schema version
4state on success.
workflow: additive is the default. It is useful for archive roots, fan-out roots that intentionally receive disjoint outputs, and roots that accumulate managed outputs over time.
workflow: replacement is useful for stable latest-style roots where the current pipeline and destination should leave only the currently planned output set for that owner. Replacement workflow is normal managed behavior and does not require --force.
Fan-out destinations are independent. If one destination fails after planning or execution begins, later destinations are still attempted. The command exits non-zero if any destination failed.
If a write fails after some outputs were written, distributor attempts cleanup before returning the error. Operators should still inspect the destination bundle path after a failed write before retrying.
Destination State Repair
Use reconcile-state when .distributor.json still records managed outputs that no longer exist in destination storage. This repairs the catalog record only; it does not restore missing files.
Preview the repair first:
go run ./cmd/distributor reconcile-state \
--config <config-path> \
--pipeline <pipeline-id> \
--destination <destination-id> \
--dry-run
Apply after reviewing the report:
go run ./cmd/distributor reconcile-state \
--config <config-path> \
--pipeline <pipeline-id> \
--destination <destination-id>
The command opens the configured destination root selected by --pipeline and --destination, reads the root .distributor.json, checks matching catalog output paths with storage metadata, reports missing managed outputs, and reports unmanaged entries under that root. It excludes .distributor.json and already managed paths from unmanaged reporting.
Without --dry-run, it removes missing managed output records from valid catalog state and rewrites .distributor.json. It never deletes destination files, adopts unmanaged files, validates output digests, or rewrites invalid state. Add --all-owners only when every catalog owner inside the selected root should be repaired.
Managed Output Pruning
Use prune when a destination config has retention.prune.enabled: true and old managed outputs should be removed according to that configured policy. Pruning is never automatic after publish.
Preview selected managed outputs first:
go run ./cmd/distributor prune \
--config <config-path> \
--pipeline <pipeline-id> \
--destination <destination-id> \
--dry-run
Apply after reviewing the report:
go run ./cmd/distributor prune \
--config <config-path> \
--pipeline <pipeline-id> \
--destination <destination-id> \
--apply
The command opens the configured destination root selected by --pipeline and --destination, reads the root .distributor.json, and plans from the selected destination's retention.prune policy. It uses managed output updated_at timestamps. When both keep_latest and older_than are configured, it preserves the newest keep_latest outputs before applying the age policy.
--dry-run does not delete outputs or rewrite state. --apply deletes only planned managed output paths for the selected pipeline/destination owner, preserves unmanaged files, preserves .distributor.json, removes confirmed deleted records from state, and updates the state timestamp. If a delete fails after earlier deletes succeed, state is rewritten only for confirmed deletions; failed and unattempted output records remain so retry remains accurate.
Dry Runs And Output Review
run --dry-run loads config, resolves credentials, discovers source bundles, opens destinations, inspects destination state, builds publish plans, and prints actions. It does not write outputs, .distributor.json, notifier events, or SSH known_hosts entries.
Review these action labels before publishing:
publish_new: destination state is absent and the destination bundle path is empty.upsert_additive: additive workflow will write planned outputs into valid catalog state.replace_catalog: replacement workflow will write planned outputs and remove omitted outputs for the current owner.skip_same: planned outputs already match valid catalog metadata, so publication will not write outputs, rewrite.distributor.json, delete files, or notify.force_replace: destructive catalog replacement selected because--forceis present for unmanaged content, a planned unmanaged path collision, invalid state, or unsupported future state.fail_unmanaged: unmanaged destination content prevents publication.fail_conflict: invalid or unsupported destination state prevents publication.error: setup, planning, or execution failed for that destination.
Text and JSON summaries count publish_new, upsert_additive, replace_catalog, skip_same, force_replace, fail_unmanaged, fail_conflict, and failed destinations separately. JSON output includes warnings, pipeline summaries, destination action records, output records, URLs when configured, final counters, and partial failure details. Fatal setup failures such as unreadable config or invalid secrets do not produce a JSON result document.
The skip_same optimization trusts valid catalog metadata. It compares owner identity, source identity, output path, kind, digest, size, generated output metadata, and URL metadata recorded in .distributor.json; it does not read destination file bytes to detect bitrot.
Fixed destinations add fixed-path warnings during dry runs, including the selected source bundle and replacement warnings when the destination root would be replaced. For fixed destinations, the resolved destination bundle path is the backend root.
Forced Replacement Workflow
Use --force only after a dry run shows the intended bounded force_replace action:
go run ./cmd/distributor run --config <config-path> --dry-run --force
go run ./cmd/distributor run --config <config-path> --force
Forced replacement can claim a non-empty destination path with no valid .distributor.json, replace planned output paths that collide with storage content not recorded in valid catalog state, and recover from invalid or unsupported future destination state. It is reserved for exceptional destructive replacement. Valid catalog-managed additive upserts and replacement workflow publishes do not require --force.
Forced replacement deletes the current destination bundle path before writing planned outputs and schema version 4 catalog state. It does not delete parent paths, sibling paths, or storage outside the destination bundle path. For fixed destinations, the destination bundle path is the configured backend root, so a forced replacement can clear that configured root. Dry-run text and JSON output report that root as target=. or destination_path: ".".
--force applies only to the current invocation. There is no config field that enables forced replacement by default.
HTTP Upload Operation
The HTTP Upload API Contract defines request and response details. distributor serve runs the HTTP upload API for pipelines whose source backend is http_upload. Top-level upload_tokens authenticate producers and allow one or more upload pipelines. Token values come from the process environment or secrets.directory, not from YAML literal values.
Start the maintained local example:
DISTRIBUTOR_EXAMPLE_UPLOAD_TOKEN=<token> \
go run ./cmd/distributor serve --config examples/http-upload-local.yml
Readiness:
curl http://127.0.0.1:8080/healthz
Upload one tar or tar.gz source bundle archive:
curl -X POST http://127.0.0.1:8080/v1/pipelines/example-http-upload/upload \
-H "Authorization: Bearer $DISTRIBUTOR_EXAMPLE_UPLOAD_TOKEN" \
-H "Content-Type: application/gzip" \
--data-binary @bundle.tar.gz
For safe producer retries, include an idempotency key that is stable for the same producer run and different for each distinct run:
curl -X POST http://127.0.0.1:8080/v1/pipelines/example-http-upload/upload \
-H "Authorization: Bearer $DISTRIBUTOR_EXAMPLE_UPLOAD_TOKEN" \
-H "Content-Type: application/gzip" \
-H "Idempotency-Key: producer.run.20260604T120000Z" \
--data-binary @bundle.tar.gz
Go producer applications can use pkg/upload instead of constructing archives and HTTP requests directly. See Upstream Producer Integration.
The maintained example client uses the local upload server, reads the token from DISTRIBUTOR_EXAMPLE_UPLOAD_TOKEN, and defaults the pipeline id to example-http-upload.
go run ./examples/upload-client
Accepted uploads return after the archive is staged and validated:
{"run_id":"example-http-upload.20260604T120000Z.abcdef12","status":"accepted"}
Poll status while the in-memory record is retained:
curl http://127.0.0.1:8080/runs/<run-id>
Status values are accepted, queued, running, succeeded, and failed. Completed records expire after server.http.retention. Expiration removes committed staged bundle directories for completed uploads. Restarting the process clears upload status, queue state, and in-memory records.
Upload admission is bounded by server.http.queue_size. Publication concurrency is bounded by server.http.max_concurrency, and the coordinator does not run two uploads for the same pipeline at the same time.
Idempotency-Key is optional for raw HTTP clients. When present, it is scoped to the token id, pipeline id, and key. Reusing the same key with the same normalized source manifest in that scope returns the original accepted run response and does not enqueue another run. Reusing the key with a different source manifest returns 409 Conflict.
The upload server accepts application/x-tar, application/gzip, and application/x-gzip. Archives are extracted into a temporary staging directory, must contain exactly one root-level manifest.json, and must validate as one complete source bundle before a run id is issued. Per-source max_upload_size bounds both uploaded archive size and extracted bundle size.
The default bind address is private loopback. Put TLS, public routing, rate limiting, and external access policy in a reverse proxy or deployment layer.
Remote Backend Notes
SSH/SFTP
SSH execution uses native SFTP. See SSH/SFTP Integration. It does not shell out to ssh, scp, or rsync.
Configure ssh_key_file, an SSH agent, or both. Agent identities are attempted first, followed by the configured key file. YAML password authentication is not supported.
The default host key policy is accept-new. During dry runs, unknown host keys may be accepted for the current connection but are not persisted. Changed host keys are fatal for strict and accept-new. host_key_policy: off disables host key checking and should be limited to controlled test environments.
S3-Compatible Storage
S3 execution uses the AWS SDK for Go v2. See S3-Compatible Storage Integration. Configure an endpoint, bucket, optional prefix, optional region, optional path-style setting, and optional explicit credential variable names.
When explicit S3 credential variable names are configured, both must resolve to non-empty values through the process environment or secrets.directory. When omitted, the AWS SDK default credential chain is used as-is.
Normal managed replacement and failed-write cleanup delete only managed output objects recorded in .distributor.json plus the state object. Forced replacement deletes objects under the bounded destination bundle prefix and then writes schema version 4 catalog state. Distributor does not manage bucket versioning or delete markers.
Secrets Operation
secrets.directory is loaded during run, serve, and configured-source validate or inspect before credential-consuming work starts. If the directory is missing, unreadable, or contains an invalid secret filename, the command fails before storage work starts.
Real process environment values take precedence over files with the same name. If the values differ and stdout is enabled, commands emit a warning naming the ignored secret variable without printing either value. The process environment is not modified.
Cleanup And Recovery
Use these recovery boundaries:
- For source validation failures, regenerate the source bundle and manifest together.
- For an empty or missing destination, rerun after fixing config or storage access.
- For unmanaged destination content, move unrelated files aside or use a different destination path before publishing.
- For invalid or unsupported destination state, inspect
.distributor.json; use--forceonly after dry-run review confirms bounded replacement is intended. - For missing managed output files recorded in state, run
reconcile-state --dry-run, then applyreconcile-stateif the missing files should no longer be considered managed. - For configured retention cleanup, run
prune --dry-run, then applyprune --applyafter reviewing the managed output list. - For failed writes, inspect the destination bundle path, remove only confirmed partial outputs if needed, then rerun
--dry-run. - For HTTP upload failures, inspect
/runs/<run-id>while retained; after expiry or restart, rely on destination state and logs/output from the publishing run.
Do not edit .distributor.json during normal recovery. Treat it as the managed state record used for comparison and safe cleanup.
For symptom-specific fixes, see Troubleshooting.