Migrate from Artifactory to CloudRepo
cloudrepo-migrate artifactory copies the local Maven and npm repositories of a JFrog Artifactory server into
CloudRepo, then reads every file back from CloudRepo and checks it against the original.
It moves every file of a Maven repository but a root archetype-catalog.xml, which the report names, and, for npm,
every version of each package with its dist-tags.
npm is tested against a stand-in for Artifactory’s npm API, not yet against a real Artifactory server.
From version 0.7.0 it also moves every tag of each local Docker repository, tested against a stand-in for
Artifactory’s Docker API, not yet against a real Artifactory server.
From version 0.7.0 it also moves every wheel and sdist of each local PyPI repository, tested against a stand-in for
Artifactory’s PyPI API, not yet against a real Artifactory server.
From version 0.8.0 it also moves a local Generic repository you name with --repo SOURCE=TARGET into TARGET, a
CloudRepo Maven repository, every file at the same path and served as it is, tested against a real Artifactory OSS
7.146.39. See Generic repositories.
One command moves your Artifactory Maven and npm repositories into CloudRepo, every file checksum-verified.
cloudrepo-migrate artifactory \ --artifactory-url https://artifactory.example.com/artifactory \ --org your-org \ --report-json report.jsonAdd --dry-run for a first run that uploads nothing. Artifactory is only read. It keeps running, unchanged, until
you repoint your builds. Using an agent? Go to For agents. On Nexus? Use
cloudrepo-migrate nexus3 or nexus2, the
same tool with the same options.
What you need
Section titled “What you need”- A CloudRepo organization. If you have none, sign up.
- A repository in CloudRepo for each Artifactory repository
you move, of the same format. Give it the Artifactory repository’s key, or map the names with
--repo. A CloudRepo repository name has lowercase letters, digits,_and-, and at most 32 characters, so a key such aslibs-release-localfits and a longer one needs--repo. Use a new, empty repository where you can: the tool puts each file at the path Artifactory has it, and a repository that allows overwrites replaces a file already there. - A repository token with Read + write on those repositories.
- Python 3.8 or later with pipx (or pip), on a machine that can reach your Artifactory server and your CloudRepo organization over the network.
- An Artifactory access token that can read the repositories. A legacy API key works too.
1. Install the tool
Section titled “1. Install the tool”cloudrepo-migrate is one command for Python 3.8 or later, with no dependencies. Install it from PyPI:
pipx install cloudrepo-migrateNo pipx? pip install cloudrepo-migrate in a virtual environment works too. Then check that it runs:
cloudrepo-migrate --versionartifactory needs version 0.5.0 or later. If you installed the tool earlier for Nexus, run
pipx upgrade cloudrepo-migrate. An older version answers invalid choice: 'artifactory'.
Docker repositories move from version 0.7.0, tested against a stand-in for Artifactory’s Docker API, not yet against
a real Artifactory server. Earlier versions list them as skipped.
PyPI repositories move from version 0.7.0, tested against a stand-in for Artifactory’s PyPI API, not yet against a
real Artifactory server. Earlier versions list them as skipped.
Generic repositories move from version 0.8.0, when --repo names them. Earlier versions list them as skipped.
2. Run it
Section titled “2. Run it”Put the credentials in the environment of the shell that runs the tool: ARTIFACTORY_ACCESS_TOKEN for Artifactory,
and CLOUDREPO_TOKEN for CloudRepo. Without an access token, the tool sends ARTIFACTORY_API_KEY instead (JFrog has
deprecated API keys, so prefer a token). The tool reads them only from the environment. No option takes a credential,
the tool never prints one or writes one to the report, and it sends the Artifactory credential only to the address you
give it. It never follows a redirect, so the credential cannot be sent on to another host.
Run the command at the top of this page once with --dry-run added. A dry run lists what would move and uploads
nothing. With CLOUDREPO_TOKEN set, it also checks the token and that each target repository exists. Then run it
without --dry-run.
| Option | Meaning |
|---|---|
--artifactory-url URL |
The base address of your Artifactory, ending in /artifactory, such as https://artifactory.example.com/artifactory. On JFrog Cloud, it is your server’s address followed by /artifactory. |
--org ORG or --cloudrepo-url URL |
Your CloudRepo organization, or its full https address. |
--repo SOURCE or SOURCE=TARGET |
Move only this Artifactory repository, to the CloudRepo repository TARGET (default: the same name). Repeat it for more. Default without it: every local Maven and npm repository, every local PyPI repository (from version 0.7.0, tested against a stand-in for Artifactory’s PyPI API, not yet against a real Artifactory server), and every local Docker repository (from version 0.7.0, tested against a stand-in for Artifactory’s Docker API, not yet against a real Artifactory server). A local Generic repository moves only when named here, from version 0.8.0, into a CloudRepo Maven repository TARGET. |
--dry-run |
List what would move. Upload nothing. |
--latest-only |
For npm, move only the version each package’s latest dist-tag names. Default without it: every version, with its dist-tags. |
--report-json PATH |
Also write the report to PATH as JSON. |
--json |
Print the report as one JSON document on standard output, and the text on standard error. |
--jobs N |
Upload N Maven files at a time (default 4). npm packages and Docker tags move one at a time. |
--work-dir DIR |
Where a Maven file, or a Docker layer, is held while it moves (default: the system temporary directory). |
--artifactory-ca-file PEM |
A CA bundle, for an Artifactory with a private certificate. |
What moves, and what does not
Section titled “What moves, and what does not”| In Artifactory | What happens |
|---|---|
| Local repository, package type Maven | Every file moves, POMs and maven-metadata.xml included, except a root archetype-catalog.xml (below). Artifactory serves each file’s .sha1 and .md5 without storing them, so the tool writes both into CloudRepo from the file’s checked bytes, and Maven finds them as it did in Artifactory. The maven-metadata.xml files move last, so a build never reads a version whose files have not landed. |
| Local repository, package type npm | Every version of each package moves, oldest first, scoped packages included. Then each dist-tag Artifactory has (latest, next and any other) that CloudRepo lacks is added. A dist-tag CloudRepo already holds is never moved or removed: if it names another version than Artifactory does, or Artifactory has no such tag, the report lists it as kept. A deprecated version keeps its message. A version unpublished from Artifactory is not in its package, so it does not move. With --latest-only, only the version latest names moves. Tested against a stand-in, not yet a real Artifactory. |
| Local repository, package type PyPI | Every wheel (.whl) and sdist (.tar.gz or .zip) moves, every version, from version 0.7.0, tested against a stand-in for Artifactory’s PyPI API, not yet against a real Artifactory server. Each is uploaded under the project name its own metadata gives, so pip finds it by the name it always had. Other files beside them, such as signatures, do not move, and the report counts them. |
| Local repository, package type Docker | Every tag of every image moves, from version 0.7.0, with the manifest and every layer it names, tested against a stand-in for Artifactory’s Docker API, not yet against a real Artifactory server. A tag that lists several platforms moves with each platform’s image. Each image keeps its name and tags, and each manifest its digest. The report counts each tag as a file. See Docker images. |
| Local repository, package type Generic | From version 0.8.0, moves only when --repo names it: every file it stores moves into TARGET, a CloudRepo Maven repository, at the same path and served as it is, a dot file and a root archetype-catalog.xml included, with no .sha1 or .md5 written beside it. Without --repo naming it, it is skipped with a reason that names the flag. See Generic repositories. |
| Remote repository | Skipped. Create a proxy repository in CloudRepo if its upstream is one CloudRepo offers. |
| Virtual repository | Skipped: it stores nothing of its own. Move the local repositories it includes. For Maven and Docker, you can then create a group repository in CloudRepo to serve them under one address. |
| Federated repository | Skipped. This version moves local repositories. |
| Any other package type | Skipped. From Artifactory, this version moves Maven, npm and Docker (from version 0.7.0, tested against a stand-in for Artifactory’s Docker API, not yet against a real Artifactory server), and PyPI from version 0.7.0, tested against a stand-in for Artifactory’s PyPI API, not yet against a real Artifactory server. A Generic repository moves from version 0.8.0 when --repo names it. For the other formats CloudRepo hosts, export and import the files as Migrate from Sonatype Nexus Repository shows for any server. |
| Files Artifactory generates itself | Skipped: any file or folder whose name starts with a dot, such as the Maven index under .index and the npm metadata under .npm. In a Generic repository a dot file moves, as a file someone stored. |
A root archetype-catalog.xml |
Not moved from a Maven repository: this version moves no archetype catalog. The report says so on that repository’s line. Deploy the catalog to CloudRepo yourself. From a Generic repository it moves, as any other file. |
Every skipped repository is in the report with its reason. The tool deletes nothing.
Docker images
Section titled “Docker images”From version 0.7.0, a local Docker repository moves image by image, tested against a stand-in for Artifactory’s
Docker API, not yet against a real Artifactory server: every tag, its manifest and every layer the manifest names. A
tag that lists several platforms moves each platform’s image first, then the list. The tool reads Artifactory through
the Docker registry API Artifactory serves for each repository under /artifactory/api/docker/, with
ARTIFACTORY_ACCESS_TOKEN (or ARTIFACTORY_API_KEY).
- Every layer is checked against its digest. The bytes the tool reads from Artifactory must hash to the digest the manifest names, and the layer is uploaded by that digest. A layer CloudRepo already holds is not uploaded again, and a layer several images share is uploaded once.
- Every manifest is read back. Each manifest is uploaded as Artifactory serves it, so it keeps its digest, then read back from CloudRepo and compared by digest.
- A manifest that is not Docker schema 2 or OCI fails its tag. The tag is reported as failed, so the exit code is
1, withmanifest REFERENCE is TYPE; CloudRepo takes Docker schema 2 and OCI manifests, whereREFERENCEis the tag or digest it read andTYPEis the media type Artifactory served, oruntyped. - A layer over 2 GiB fails its tag. The tag is reported as failed, so the exit code is
1, withblob DIGEST is SIZE bytes; CloudRepo takes a blob of at most 2147483648. - Remote, virtual and federated repositories are skipped, with the reason. A Docker remote repository is skipped
as a
proxy of a remote: create a proxy repository in CloudRepo instead. A Docker virtual repository is skipped as avirtual repository: move the local repositories it includes. A Docker federated repository is skipped as afederated repository. CLOUDREPO_TOKENmust be a CloudRepo repository token. Create a repository token with Read + write on the target Docker repository. The tool signs in to CloudRepo’s Docker registry with it, so there is no username to set.
Create a Docker repository in CloudRepo for each Artifactory Docker repository you move. Then move one with:
cloudrepo-migrate artifactory \ --artifactory-url https://artifactory.example.com/artifactory \ --org your-org \ --repo docker-local \ --report-json report.jsonReplace docker-local with the key of your Artifactory Docker repository, or write docker-local=TARGET to move it
into a CloudRepo repository named TARGET. Add --dry-run first to count the tags and bytes without uploading
anything. Run the same command again to resume: a tag CloudRepo already holds at the same digest is counted as
already present, and nothing of it is uploaded. After the move, Docker clients pull each image from CloudRepo with
repositories/<repository>/ in its name, as Pull Docker images shows.
Generic repositories
Section titled “Generic repositories”From version 0.8.0, a local Generic repository moves when you name it with --repo, into a CloudRepo Maven
repository. CloudRepo has no Generic repository type: a Maven repository serves any file at any path,
so installers, archives and other binaries keep the paths your scripts already fetch. Tested against a real
Artifactory OSS 7.146.39.
- Every file, at the same path. Every file the Generic repository stores moves into the CloudRepo Maven
repository at the path Artifactory has it, and CloudRepo serves it as it is, so only the address before the path
changes. A file or folder whose name starts with a dot, and a root
archetype-catalog.xml, move too: in a Generic repository each is a file someone stored. - Every file is checked against the checksum Artifactory recorded. Before the upload, the bytes must match the SHA-256 and SHA-1 Artifactory lists for the file, and Artifactory must have listed one of them: a file with neither is not uploaded. After the upload, the tool reads the file back from CloudRepo and compares its SHA-1 and SHA-256.
- No checksum file is added. Unlike for a Maven file, the tool writes no
.sha1or.md5beside a Generic file: those are a Maven client’s, and only what Artifactory stores moves. A checksum file Artifactory stores moves as a file, and must name the digest of the file beside it. - A file CloudRepo cannot store fails, in a dry run too. A path with a character outside
A-Z a-z 0-9 + _ - . ! * ' ( ) / @ ~, longer than 1024 characters, or with an empty,.or..segment, and a file over 50,000,000,000 bytes, the most CloudRepo stores in one file, are each listed as a failure with the path, so the exit code is1. A--dry-runlists the same failures, before anything moves. - Unnamed, it is skipped, with the flag to use. Without
--reponaming it, a Generic repository is skipped asgeneric format is not supported by default: name it with --repo KEY=TARGET to move its files into a CloudRepo Maven repository TARGET, each at the same path. --repomoves only what it names. Name your other repositories beside the Generic one, or move the Generic one in a run of its own.
Create a Maven repository in CloudRepo for each Artifactory Generic repository you move. Then move one with:
cloudrepo-migrate artifactory \ --artifactory-url https://artifactory.example.com/artifactory \ --org your-org \ --repo generic-local=installers \ --report-json report.jsonReplace generic-local with the key of your Artifactory Generic repository, and installers with the CloudRepo
Maven repository it moves into. Add --dry-run first to count the files and bytes, and to list each file CloudRepo
cannot store, without uploading anything. Run the same command again to resume: a file CloudRepo already holds with
the same bytes is counted as already present. After the move, fetch each file from CloudRepo at its same path, as
Raw files over HTTP shows.
How every file is checked
Section titled “How every file is checked”- Before the upload. A file’s bytes must match the SHA-256 and SHA-1 Artifactory lists for it. An npm tarball
must also match its package’s
dist.shasumand, when the package has one, itsdist.integrity. A file that does not match is not uploaded. - After the upload. The tool reads the file back from CloudRepo and compares its SHA-1 and SHA-256 with the bytes it took from Artifactory. A wheel or sdist must also be listed in its project’s index, where pip looks for it.
- Checksum files. The
.sha1and.md5the tool writes for a Maven repository come from the bytes it downloaded and checked, never from Artifactory’s numbers alone, and are read back the same way. For a Generic repository it writes none. - Docker. Each layer must hash to the digest its manifest names before it is uploaded, and each manifest is read back from CloudRepo and compared by digest, as Docker images says.
- Generic. A Generic file is checked as a Maven file is, and Artifactory must have listed its SHA-256 or SHA-1, or it is not uploaded, as Generic repositories says.
Any mismatch fails the run and names the file. The run then exits with 1, so a script or an agent cannot read a
failed check as success.
Resume after a failure
Section titled “Resume after a failure”Run the same command again. A file CloudRepo already holds is counted as already present, and not uploaded
again, only after the tool has read it back and found the same bytes. A repository that refuses to overwrite a
release answers 409 for a file it holds. The tool counts that file as moved only when the bytes CloudRepo
holds match the source. A Docker tag CloudRepo already holds at the same digest is counted as already present on
that one digest check, with nothing read back.
The end report
Section titled “The end report”The last line of the output is the result:
Moved N repositories, F files, B bytes in M minutes; V of F verified, K failed.N, F and B are the repositories, files and bytes that moved, M is the elapsed time, V is the number of
Maven, npm and PyPI files read back and matching plus the number of Docker tags verified, and K is the number that
failed. A Docker tag is verified when its manifest’s digest matches and its blobs were checked by digest before
upload, or when CloudRepo already held it at the same digest; layers are not read back. Above it is one line for each repository: its
Artifactory key, its CloudRepo name, its format, its counts and, for a skipped repository, why. A line
REPO PACKAGE: kept: TAG -> VERSION (source: VERSION or none) names an npm dist-tag CloudRepo already holds
that the tool left as it was. It is not a failure.
--report-json writes the same report as JSON, and --json prints it as one JSON document on standard output (the
text then goes to standard error). A script reads these keys:
| Key | Holds |
|---|---|
exit_code |
The exit code of the run, as in If something fails. |
error |
The message of a run that stopped before its report, such as a refused token. Such a document holds only exit_code and error. |
totals.files, .verified, .failed |
The counts in the last line. |
failures |
One entry for each failed file, with its repository, path and reason. Empty when nothing failed. |
kept |
One entry for each npm dist-tag CloudRepo already holds and the tool left as it was: its repository, path (the package), tag, version (CloudRepo’s) and source (the version Artifactory tags, or null when Artifactory has no such tag). Not a failure. |
repositories[].action |
moved, would move (a dry run), skipped, ready or blocked. |
repositories[].reason |
Why a repository was skipped or blocked, a root archetype-catalog.xml that did not move, the count of other PyPI files that did not move, how many Docker images a repository holds, in a dry run what moves from an npm repository, or, with --latest-only, the count of other npm versions that did not move. |
If something fails
Section titled “If something fails”The exit code says which kind of failure it was:
| Exit code | Meaning |
|---|---|
0 |
Everything moved, every Maven, npm and PyPI file was read back and matched, and every Docker tag’s manifest matched its digest. |
1 |
At least one file failed. The output lists each one with its path. Fix the cause and run the same command again. |
2 |
A problem before anything moved, such as a missing target repository or a refused token. Nothing was uploaded. |
130 |
Stopped with Ctrl-C. Run the same command again to resume. |
The messages you are most likely to see:
| The output says | What to do |
|---|---|
CloudRepo has no repository NAME: create it as ... |
Create the repository in CloudRepo, in the format named, then run again. Or map the Artifactory repository to another with --repo. |
CloudRepo has no Docker repository NAME: create one at ... |
Create a Docker repository with that name in CloudRepo, then run again. Or map the Artifactory repository to another with --repo. |
... is not a valid CloudRepo repository name |
The Artifactory key is not a valid CloudRepo name. Choose a name with --repo SOURCE=TARGET. |
CloudRepo refused the token |
Check CLOUDREPO_TOKEN, and that the token belongs to this organization. |
the token cannot reach CloudRepo repository NAME, or cannot publish |
Give the token Read + write on that repository. |
Artifactory refused the login |
Set ARTIFACTORY_ACCESS_TOKEN to a token that can read the repository. |
the bytes Artifactory served ... do not match the ... Artifactory recorded |
The copy in Artifactory does not match its own record, so the tool did not upload it. Check that file in Artifactory. |
Artifactory recorded no sha1 or sha256 for the file, so it cannot be checked before the upload |
A Generic file Artifactory lists no SHA-256 or SHA-1 for, so the tool did not upload it. Check that file in Artifactory. |
CloudRepo cannot store this path (...) |
The path has a character outside the ones the message lists, is longer than 1024 characters, or has an empty, . or .. segment, so the file does not move. The other files do. Rename it in Artifactory to move it; until then the run keeps exiting 1. |
the file is SIZE bytes, over the 50000000000 bytes CloudRepo stores in one file |
The file is larger than CloudRepo stores in one file, so it does not move. The other files do, and the run keeps exiting 1 while the file is in the repository you move. |
answered 302, a redirect to ... |
Artifactory sent the download to another address, as some storage setups do. The tool never follows a redirect, so your credential stays with Artifactory. Email support@cloudrepo.io with the message. |
the package has no usable latest dist-tag |
Only with --latest-only: the package has no latest tag in Artifactory that names a version it lists, so the tool cannot choose a version. Tag one and run again, or run without --latest-only. |
dist-tags the source has, not added: ... |
A dist-tag Artifactory has, and CloudRepo lacks, names a version that did not move, whose own failure is listed too: fix that one and run the same command again. A tag that names a version Artifactory does not list stays unset until you retag it in Artifactory. |
CloudRepo already holds different bytes for this path and refuses overwrites |
The file in CloudRepo is not the one in Artifactory, and the repository will not replace it. Decide which copy is right, then email support@cloudrepo.io with the path. |
the bytes CloudRepo serves back differ from the source |
Run the same command again. If the same file fails twice, email support@cloudrepo.io with its path. |
After the move
Section titled “After the move”The files are in CloudRepo, and your builds still point at Artifactory. Repoint them one project at a time, as step 5 of Migrate from Sonatype Nexus Repository shows for each build tool, with your Artifactory address where that page has the Nexus one, and leave Artifactory running until your builds pass against CloudRepo.
For agents
Section titled “For agents”Give your agent this instruction. It carries the address of this page as plain text, so the agent reads this page and nothing else:
Migrate my Artifactory to CloudRepo. Follow https://www.cloudrepo.io/docs/migrate/from-artifactory.md. Read my credentials from the environment variables ARTIFACTORY_ACCESS_TOKEN and CLOUDREPO_TOKEN, and ask me for the Artifactory URL and the CloudRepo organization if you do not have them.The invocation, in two runs. Replace the address and the organization with the owner’s:
cloudrepo-migrate artifactory --artifactory-url https://artifactory.example.com/artifactory --org your-org --dry-run --jsoncloudrepo-migrate artifactory --artifactory-url https://artifactory.example.com/artifactory --org your-org --jsonEach run prints one JSON document on standard output, its result, and the text report on standard error. The end report names the keys.
Steps:
- Check that
python3 --versionis 3.8 or later. Ifcloudrepo-migrate --versiondoes not answer, install the tool with the command in Install the tool, and check again. If it answers a version older than 0.5.0, runpipx upgrade cloudrepo-migrate, and check again. Ifcloudrepo-migrate artifactory --helpdoes not list--json, runpipx upgrade cloudrepo-migrate. If it still does not, run each command with--report-json report.jsonin place of--json, and readreport.json: the same document, withoutexit_code. - Check that
CLOUDREPO_TOKENis set without printing it, for example withtest -n "$CLOUDREPO_TOKEN". Do the same forARTIFACTORY_ACCESS_TOKEN, or forARTIFACTORY_API_KEYif the owner uses an API key. - Run the first command, the dry run. It reads Artifactory and writes nothing to CloudRepo. Expect exit code
0, and in the documentexit_code0anddry_runtrue. Each entry inrepositorieswith the actionwould moveis a repository that will move, with itsformatandfiles; each with the actionskippedsays why in itsreason. On exit code2, readerror, or thereasonof each repository with the actionblocked, in If something fails. If a target repository is missing, ask the owner to create it in the portal, then repeat the dry run. - Run the second command.
- Verify, all of these, in the document it printed:
exit_codeis0, and so is the exit code of the command.failuresis empty,totals.failedis0andtotals.verifiedequalstotals.files.- No entry in
repositorieshas the actionblocked.
- Tell the owner
totals.repositories,totals.filesandtotals.verified, every repository with the actionskippedand its reason, every entry inkept(a dist-tag CloudRepo already holds, left as it was), every PyPI repository whose reason counts other files that did not move and, with--latest-only, every npm repository whose reason counts versions that did not move.
If the exit code is 1, read each entry in failures, with its path and reason, remove its cause, and run the
second command again, which resumes. If the same file fails twice, stop and report its path and reason to the owner.
Do not put a credential on a command line, in a file or in your answer. Do not change anything in Artifactory. Do not repoint builds or switch CI to CloudRepo until the owner tells you to.