# shdctl artifact command

> Mirror Docker images and ORAS artifacts between registries, and verify the result

Before you deploy, sync artifacts from the source registry (`artifactSync.sourceRepository`) to the target registry (`configuration.kubernetes.docker.repository`). Set `artifactSync.sourceRepository` in the manifest: `shdctl manifest schema` lists a default for it, but every sync subcommand — the pull-straight case below included — fails with `source registry is required` when it is left out. Use one of these subcommands for the artifact type:

* `shdctl artifact sync images` — Docker container images (uses `docker pull`/`tag`/`push`)
* `shdctl artifact sync oras` — ORAS artifacts (registry-to-registry copy via `oras-go`)

The manifest's `docker.namespace` is used to prefix image paths on the target registry when set.

To pull straight from Unity's registry rather than mirror, set `configuration.kubernetes.docker.repository` to the source registry and leave `docker.namespace` out. Every sync subcommand, `verify` included, then logs that there is nothing to mirror and exits 0 without contacting either registry — as it does whenever `--target-registry` names the source. The manifest schema refuses a namespace there — every command that reads the manifest, `manifest validate`, `release generate` and `artifact sync` among them, fails naming the field — because generation prefixes every image reference with it whatever the registry, and nothing mirrors images to that path, so the deploy could not pull them. Keep the `docker` block itself: without it every sync subcommand fails, and generation renders image references with no registry at all.

`sync oras` and `sync images` also expand any release manifest the release lists. `sync oras` mirrors the artifacts the manifest names, and `sync images` mirrors the Docker images those artifacts run.

The manifest names those images; `versions.yaml` does not. An app and the image it runs therefore reach your registry together, instead of the app arriving alone and failing the first time it runs. A release whose manifest names no images syncs only what `versions.yaml` lists.

To expand the list, both subcommands read the manifest from the source registry. They contact it even when you pass `--dry-run`.

Two further subcommands check a sync rather than perform one: `shdctl artifact sync preflight` proves your credentials work by syncing one artifact of each kind, and `shdctl artifact sync verify` compares what your target registry holds against what your manifest requires without writing to it.

All four subcommands (`images`, `oras`, `preflight`, `verify`) accept:

* `--skip-version-check`: Skip shdctl version compatibility check against the release package

## Preflight

Verify your Docker and ORAS credentials are working before you run a full sync:

```sh
shdctl artifact sync preflight
```

The command picks one Docker image and one ORAS artifact from the release and attempts to sync them — a real copy into your registry, not a simulation. The image can be any in the release, whether or not your manifest enables the feature that needs it, and its local copy is removed once the push succeeds. A successful run confirms pull/push credentials. On failure, the command prints troubleshooting hints, such as expired tokens or a missing `docker login`.

**Options:**

* `--target-registry`: Target registry URL (defaults to manifest value)
* `--extracted-release`: Path to extracted release (defaults to `./extracted-release`)

`preflight` and `verify` answer different questions, and in that order: preflight proves the credentials, verify reports the content. Run preflight first — refer to [Verify the target registry](#verify-the-target-registry) for why a verify run against a registry you cannot read is not worth reading.

## Sync Docker images

1. Authenticate to both source and target registries before you sync:

   ```sh
   docker login <source-registry>
   docker login <target-registry>
   ```

2. Preview the commands that would be executed:

   ```sh
   shdctl artifact sync images --dry-run
   ```

3. Run the sync:

   ```sh
   shdctl artifact sync images
   ```

Each image is pulled as `linux/amd64` (`docker pull --platform linux/amd64`), so your registry receives a single-platform amd64 image under each tag, whatever the source holds.

### What gets mirrored

The mirror is scoped to your manifest: an image that only a feature you have turned off needs is not copied. Five features gate images this way, each read from one manifest field:

| Images                                                     | Mirrored when                                                                                                                          |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Istio (istiod, proxy, ztunnel, CNI)                        | `configuration.networking.serviceMesh.istio.enabled: true`                                                                             |
| Prometheus and Grafana                                     | `configuration.monitoring.prometheus.enabled: true`                                                                                    |
| Log collection (Loki, Alloy)                               | `configuration.monitoring.logCollection.enabled: true`                                                                                 |
| Database monitoring (the PMM server)                       | `configuration.monitoring.database.enabled: true`                                                                                      |
| The in-cluster object store (garage and its bootstrap job) | `configuration.objectStore.provider` is anything but `s3` — including absent, so this one is mirrored unless you bring your own bucket |

Every other image in the release is mirrored on every run — including `percona/pmm-client`, the sidecar both database charts pin whether or not database monitoring is enabled, so expect it in a monitoring-off registry.

A run that skips anything reports what it left out. The summary goes to stderr, so it never lands in the middle of the command list a `--dry-run` writes to stdout. For a manifest with Istio, Prometheus, log collection, and database monitoring off, which keeps the in-cluster object store:

```text
Mirroring 91 of 112 versions.yaml entries. Skipped 21:
  dbMonitoring                 1
  istio                        4
  logCollection                6
  logCollection,prometheus     1
  prometheus                   9
Run with --all to mirror every image in the release.
```

Each row names a feature — or, where an image serves more than one, the set of features of which none is enabled — and counts the entries skipped under it. The header counts `versions.yaml` entries rather than images because two entries can name the same reference; the de-duplicated image count is logged separately. Nothing is printed when nothing was skipped.

Pass `--all` to mirror the release's full image set regardless of what your manifest enables. Use it for a registry shared by deployments with different feature sets, or to keep the option of turning a feature on later without going back to Unity's registry for its images.

Otherwise, turning a feature on means re-running `shdctl artifact sync images`: the images it needs were never mirrored, and a deploy cannot pull what is not there. `shdctl artifact sync verify` reports exactly which images a registry is missing.

**Options:**

* `--dry-run`: Show commands without executing (default: false)
* `--all`: Mirror every image in the release, ignoring which features the manifest enables (default: false)
* `--target-registry`: Target registry URL (defaults to manifest value)
* `--extracted-release`: Path to extracted release (defaults to `./extracted-release`)
* `--name`: Sync only images whose target repository (`<target-registry>/<namespace>/<path>`) contains this string, or whose full target reference — as `verify` lists it — equals it. A string that matches nothing syncs nothing and exits 0
* `--cleanup`: Remove local images (`docker rmi`) after each push (default: false)
* `--skip-existing`: Skip images already present on the target registry, checked with `docker manifest inspect` (default: false)
* `--concurrency`: Number of images to sync in parallel. When the flag is omitted, the manifest's `artifactSync.concurrency` applies if it is set, and `1` (sequential) otherwise. `shdctl manifest schema` lists a default of `5` for that field, but a field you leave out is not filled in — set it to sync in parallel.

Example for CI: skip existing, cleanup disk.

```sh
shdctl artifact sync images --skip-existing --cleanup
```

## Sync ORAS artifacts

ORAS artifact sync reads authentication from Docker's credential store (`~/.docker/config.json`) automatically. Run `docker login` for both source and target registries before you sync.

```sh
shdctl artifact sync oras --dry-run
```

This prints each copy as the equivalent `oras copy <source> <target>` command. The real run copies in-process, so the `oras` CLI is not needed.

```sh
shdctl artifact sync oras
```

**Options:**

* `--dry-run`: Show commands without executing (default: false)
* `--target-registry`: Target registry URL (defaults to manifest value)
* `--extracted-release`: Path to extracted release (defaults to `./extracted-release`)
* `--name`: Sync only ORAS artifacts whose name or target reference contains this string. A string that matches nothing syncs nothing and exits 0
* `--concurrency`: Number of artifacts to sync in parallel. When the flag is omitted, the manifest's `artifactSync.concurrency` applies if it is set, and `1` (sequential) otherwise — as for `sync images`, the schema's listed default of `5` is not applied.

## Verify the target registry

Compare what your target registry holds against what your manifest requires. Authenticate to the target registry first (`docker login <target-registry>`):

```sh
shdctl artifact sync verify
```

It probes the target registry for every image in the release — including the ones the release manifest names, which `sync images` mirrors — and reports:

* **required and present** — a count.
* **required and MISSING** — listed one per line. A deploy cannot pull these, so the command exits non-zero and a CI pipeline can gate a deploy on it. Mirror them with `shdctl artifact sync images`.
* **present but not required by this manifest** — listed one per line. Not a failure, and nothing to fix: the residue of an earlier `--all` run, of a feature you have since turned off, or of `preflight`, which copies one image drawn from the whole release. Sync never deletes, so removing them is a registry-side operation.
* **release manifests this target could not answer for** — listed one per line. The images they name went unchecked, so the command exits non-zero rather than call the registry clean. Mirror them with `shdctl artifact sync oras`.

It reads those manifests from the target registry, not the source, so this command needs nothing from Unity's registry and works on an air-gapped site. Run it after `shdctl artifact sync oras`, which is what copies them there.

Counts are unique image references after de-duplication rather than `versions.yaml` entries — two entries can name the same image. The command only reads the registry: it writes nothing and deletes nothing.

**Options:**

* `--target-registry`: Target registry URL (defaults to manifest value)
* `--extracted-release`: Path to extracted release (defaults to `./extracted-release`)
* `--concurrency`: Number of images to probe in parallel. When the flag is omitted, the manifest's `artifactSync.concurrency` applies if it is set, as it does for the other sync subcommands, and `4` otherwise — higher than `sync images` because a probe is a metadata read, costing no disk and no image transfer.

Two things to know before acting on a report:

**Everything missing usually means credentials, not an empty registry.** The probe is a `docker manifest inspect`, which cannot tell "not found" from "not authorized", so an absent or expired registry credential reads exactly like a registry holding nothing and would send you off to re-mirror images you already have. When every required image comes back missing and nothing extraneous was found, `verify` says so and points at `preflight`. That is the runbook order: `shdctl artifact sync preflight` proves the credentials, then `shdctl artifact sync verify` reports the content.

**The "present but not required" list is not a full audit of your registry.** It covers only the images this release defines: a probe asks about one reference at a time and cannot enumerate a registry. Tags left behind by earlier releases are not reported, so an empty list means "nothing this release defines is here unnecessarily", not "this registry is clean".

## Full sync example with ECR

ECR creates a repository on push only where a repository creation template covers its path. With the AWS baseline example in the release package (`extracted-release/platform/shd/aws-baseline-example/`), set `configuration.kubernetes.docker.namespace` to its `ecr_repository_prefix` output, the prefix its template covers.

```sh
# Authenticate to both registries (Docker prompts for the password)
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account>.dkr.ecr.us-east-1.amazonaws.com
docker login -u <user> uccmpprivatecloud.azurecr.io

# Preflight: verify authentication works
shdctl artifact sync preflight

# Sync Docker images (only those your manifest's enabled features require)
shdctl artifact sync images --skip-existing --cleanup

# Sync ORAS artifacts (reads Docker credential store automatically)
shdctl artifact sync oras

# Confirm the registry now holds every image the manifest requires
shdctl artifact sync verify
```

In automated environments, pipe the password from a secret store with `--password-stdin` instead of passing `-p <password>` on the command line. The `-p` flag writes the password to your shell history and to the process list (visible to `ps aux`), and Docker prints `WARNING! Using --password via the CLI is insecure` when you use it.
