|
| 1 | +# Flux Mirror Sync Command |
| 2 | + |
| 3 | +The `flux-mirror sync` command mirrors Helm charts and OCI artifacts between registries based on a |
| 4 | +declarative YAML config. The command is idempotent, re-running against the same config produces |
| 5 | +the same destination state, copying only what's missing or drifted. |
| 6 | + |
| 7 | +See the [config specification](./config.md) for the YAML schema. |
| 8 | + |
| 9 | +## Synopsis |
| 10 | + |
| 11 | +``` |
| 12 | +flux-mirror sync [-c|--config PATH] [flags] |
| 13 | +``` |
| 14 | + |
| 15 | +## Configuration source |
| 16 | + |
| 17 | +The config file path is resolved in the following order: |
| 18 | + |
| 19 | +1. `--config` / `-c PATH` flag. |
| 20 | +2. `FLUX_MIRROR_CONFIG` environment variable. |
| 21 | + |
| 22 | +```bash |
| 23 | +flux-mirror sync -c examples/podinfo.yaml |
| 24 | +FLUX_MIRROR_CONFIG=examples/podinfo.yaml flux-mirror sync |
| 25 | +``` |
| 26 | + |
| 27 | +## Authentication |
| 28 | + |
| 29 | +Auth is read from the ambient Docker config: |
| 30 | + |
| 31 | +- `~/.docker/config.json` (or the `DOCKER_CONFIG` env var if set). |
| 32 | +- Any configured credential helpers (e.g., `docker-credential-osxkeychain`, `docker-credential-ecr-login`, `docker-credential-gcloud`). |
| 33 | + |
| 34 | +Log in once with `docker login`, `oras login`, etc. and `flux-mirror` picks up the credentials. |
| 35 | + |
| 36 | +## Flags |
| 37 | + |
| 38 | +| Flag | Default | Description | |
| 39 | +|---------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------| |
| 40 | +| `-c, --config PATH` | — | Path to the YAML config file. Falls back to `FLUX_MIRROR_CONFIG`. | |
| 41 | +| `-o, --output text\|yaml\|json` | `text` | Output format. `text` is human-friendly; `yaml` and `json` print the structured `Result` to stdout. | |
| 42 | +| `--concurrency N` | `4` | Maximum number of copy operations to run in parallel within a single config entry. Entries themselves are processed sequentially. | |
| 43 | +| `--retries N` | `3` | Maximum number of retry attempts per job, bounded by `--timeout`. | |
| 44 | +| `--overwrite` | `false` | Force `overwrite: true` on every entry, regardless of per-entry config. See [Overwrite Behavior](./config.md#overwrite-behavior). | |
| 45 | +| `--dry-run` | `false` | Run the plan and comparison pipeline without performing any writes. Reported as `would-copy` / `would-overwrite` in the output. | |
| 46 | +| `--verbose` | `false` | Emit a structured log line per operation (entry started, mirroring tag, tag done, entry summary, sync complete) on stderr. Suppresses the spinner. | |
| 47 | +| `--no-progress` | `false` | Disable the live progress spinner. Per-job lines and the Summary still print. | |
| 48 | +| `--insecure` | `false` | Allow plaintext HTTP and skip TLS verification. **Test/dev only.** | |
| 49 | +| `--timeout DURATION` | `5m` | Per-job total budget covering all retry attempts. | |
| 50 | + |
| 51 | +## Output |
| 52 | + |
| 53 | +### Text mode (default) |
| 54 | + |
| 55 | +Pretty-print mode: |
| 56 | + |
| 57 | +``` |
| 58 | +✓ ghcr.io/stefanprodan/charts/podinfo:6.10.2 (skipped) |
| 59 | +→ localhost:5050/charts/podinfo:6.10.2 |
| 60 | +✓ ghcr.io/stefanprodan/charts/podinfo:6.11.0 (copied) |
| 61 | +→ localhost:5050/charts/podinfo:6.11.0 |
| 62 | +✗ ghcr.io/stefanprodan/charts/podinfo2 — plan failed: NAME_UNKNOWN |
| 63 | +Summary: 1 copied, 1 skipped, 1 failed in 4.2s. |
| 64 | +``` |
| 65 | + |
| 66 | +### Verbose mode |
| 67 | + |
| 68 | +`--verbose` replaces the pretty output with a full diagnostic log stream on stderr. |
| 69 | + |
| 70 | +Every layer push, blob check (existing vs pushed), manifest digest, fallback-tag update for referrers, |
| 71 | +and registry-side warning is logged. Reach for this when diagnosing TLS, auth, missing-blob, or push-rejection issues. |
| 72 | + |
| 73 | +``` |
| 74 | +2026/05/13 09:00:00 sync started entries=3 concurrency=4 retries=3 timeout=5m0s |
| 75 | +2026/05/13 09:00:00 mirroring tag src=ghcr.io/foo/bar:1.0 dst=localhost:5050/bar:1.0 |
| 76 | +2026/05/13 09:00:00 Copying from ghcr.io/foo/bar:1.0 to localhost:5050/bar:1.0 |
| 77 | +2026/05/13 09:00:00 pushed blob: sha256:fdf53ef8e04176eedbd42713efb2d002f1741c310627b38f444c6f6d92a598f7 |
| 78 | +2026/05/13 09:00:00 existing blob: sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a |
| 79 | +2026/05/13 09:00:00 localhost:5050/bar:1.0: digest: sha256:455d6a04fe43df4a93b40e1f92d65b2a9befaa0348e5a461295d961161ebf475 size: 829 |
| 80 | +2026/05/13 09:00:00 updating fallback tag sha256-455d6a04... with new referrer |
| 81 | +2026/05/13 09:00:01 tag done src=ghcr.io/foo/bar:1.0 outcome=copied elapsed=812ms |
| 82 | +… |
| 83 | +2026/05/13 09:00:04 sync complete entries=3 copied=1 overwritten=0 skipped=1 drifted=0 failed=1 duration=4.213s |
| 84 | +``` |
| 85 | + |
| 86 | +### Structured output |
| 87 | + |
| 88 | +`-o yaml` and `-o json` print the full `Result` to stdout — entries with their per-outcome tag lists and any failures |
| 89 | +Suitable for piping into another tool. |
| 90 | + |
| 91 | +```bash |
| 92 | +flux-mirror sync -c config.yaml -o json | jq '.entries[].outcomes.copied' |
| 93 | +``` |
| 94 | + |
| 95 | +## Outcomes |
| 96 | + |
| 97 | +Each tag job lands in exactly one of these buckets: |
| 98 | + |
| 99 | +| Outcome | Meaning | |
| 100 | +|-------------------|-----------------------------------------------------------------------------------------------| |
| 101 | +| `copied` | Destination did not have the tag; mirrored from source. | |
| 102 | +| `overwritten` | Destination had a different digest; replaced (only with `overwrite: true`). | |
| 103 | +| `skipped` | Destination already had the same digest; nothing to do. | |
| 104 | +| `drifted` | Destination has a different digest, `overwrite: false` — left alone, surfaced in the summary. | |
| 105 | +| `would-copy` | Dry-run forecast: would have been copied. | |
| 106 | +| `would-overwrite` | Dry-run forecast: would have been overwritten. | |
| 107 | + |
| 108 | +Plan-time failures (e.g., source registry rejected a `ListTags`) are reported as a |
| 109 | +single `✗ <entry> — plan failed: <err>` line and counted toward `failed`. |
| 110 | + |
| 111 | +## Exit codes |
| 112 | + |
| 113 | +| Code | Meaning | |
| 114 | +|------|-----------------------------------------------------------------------------------------------------------------------| |
| 115 | +| `0` | Clean run — every tag was copied or skipped as expected, no drift, no failures. | |
| 116 | +| `1` | At least one tag job failed (network error, push rejected, retries exhausted, plan failure). | |
| 117 | +| `2` | No failures, but at least one tag drifted with `overwrite: false`. The destination is out of date relative to source. | |
| 118 | + |
| 119 | +Failures take precedence over drift. `--dry-run` does not bump the exit code for `would-copy` / `would-overwrite`, |
| 120 | +but drift detection still produces `2`. |
| 121 | + |
| 122 | +## Examples |
| 123 | + |
| 124 | +### Mirror latest releases of an OCI artifact |
| 125 | + |
| 126 | +```yaml |
| 127 | +# config.yaml |
| 128 | +apiVersion: mirror.fluxcd.io/v1alpha1 |
| 129 | +kind: Config |
| 130 | +artifacts: |
| 131 | + - source: ghcr.io/stefanprodan/podinfo |
| 132 | + destination: localhost:5050/podinfo |
| 133 | + selector: |
| 134 | + semver: ">=6.9.0" |
| 135 | + limit: 3 |
| 136 | + includeReferrers: true |
| 137 | +``` |
| 138 | +
|
| 139 | +```bash |
| 140 | +flux-mirror sync -c config.yaml |
| 141 | +``` |
| 142 | + |
| 143 | +### Preview without writing |
| 144 | + |
| 145 | +```bash |
| 146 | +flux-mirror sync -c config.yaml --dry-run -o yaml |
| 147 | +``` |
| 148 | + |
| 149 | +### Force-resync drifted tags |
| 150 | + |
| 151 | +```bash |
| 152 | +flux-mirror sync -c config.yaml --overwrite |
| 153 | +``` |
| 154 | + |
| 155 | +### CI-friendly invocation |
| 156 | + |
| 157 | +```bash |
| 158 | +flux-mirror sync -c config.yaml --no-progress |
| 159 | +``` |
0 commit comments