REST API, Repository Provisioning, Security Automation, Pagination, and Infrastructure-as-Code Patterns: Guided Hands-On Workflow and Core Operations
Practice the complete API lifecycle on disposable resources: inspect health and repositories, create and update a Raw hosted repository, enumerate content through continuation-token pagination, create a narrow lab role/user, and convert the workflow into a rerunnable reconciler.
Learning objectives
- Perform read-only API preflight and preserve evidence before mutation.
- Create, update, verify, and safely delete a disposable Raw hosted repository through REST.
- Enumerate components/assets across every continuation-token page.
- Create a narrow local role/user from current security APIs and test authorization.
- Write and run an idempotent Python reconciler twice with a no-op second run.
Version baseline (26 August 2026). These lessons
use Sonatype Nexus Repository 3.95.2-01 as the
dated self-hosted reference point, verified from Sonatype's public
release repository, with Java 21 as the current runtime requirement.
The mandatory lab assumes a small, disposable, loopback-only
self-hosted Community Edition installation using H2 and a file blob
store. The local instance's own
/service/rest/swagger.json is authoritative for the
exact endpoint schema you are about to call; API fields, repository
recipes, security endpoints, and edition entitlements can change
between releases.
Automation safety boundary. Do not put real passwords, user-token passcodes, Authorization headers, production base URLs, or employer namespaces into source files or lesson evidence. The examples use environment variables, synthetic names, loopback URLs, and disposable resources. User Tokens are a Pro capability in current self-hosted Nexus Repository; Community labs therefore use a disposable local account/password only for bootstrap and immediately reduce privileges for the service identity.
Pagination documentation note. Current Sonatype
pages agree on the continuationToken contract but
currently disagree on the documented default item count for
components/assets. The robust automation rule is independent of page
size: process every returned items array, pass the
returned token unchanged, and stop only when
continuationToken is null.
1. Disposable lab contract
Use the same local self-hosted Community instance from earlier chapters or a fresh archive installation. This lesson requires no Docker, Kubernetes, cloud account, paid CI, Nexus Pro, or public DNS.
| Item | Lab value |
|---|---|
| Nexus URL | http://127.0.0.1:8081 |
| Repository | learner-api-hosted (Raw hosted) |
| Blob store | default on disposable file storage |
| Role | learner-api-publisher |
| Service user | svc-learner-api |
| Artifact namespace | learner-example/ch20/ |
Set bootstrap credentials only in the current shell. Use the actual disposable lab password; never commit it.
export NEXUS_URL='http://127.0.0.1:8081'
export NEXUS_ADMIN_USER='admin'
read -r -s -p 'Disposable Nexus admin password: ' NEXUS_ADMIN_PASSWORD
export NEXUS_ADMIN_PASSWORD
printf '\n'
test -n "$NEXUS_ADMIN_PASSWORD" && echo 'bootstrap secret is present'
2. Preflight: prove the target before a write
set -euo pipefail
curl --fail --silent --show-error "$NEXUS_URL/service/rest/swagger.json" --output /tmp/nexus-swagger.json
curl --fail --silent --show-error -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" "$NEXUS_URL/service/rest/v1/status"
curl --fail --silent --show-error -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" "$NEXUS_URL/service/rest/v1/status/writable"
curl --fail --silent --show-error -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" "$NEXUS_URL/service/rest/v1/repositories" | python -m json.tool
If the base URL is not loopback/private, writability fails, or
OpenAPI lacks /v1/repositories/raw/hosted, stop and
reconcile the lab rather than modifying commands until they “work.”
3. Read the repository schema before creating it
Use Swagger to confirm the request model for
POST /v1/repositories/raw/hosted. In the 3.95.x family,
a representative hosted payload is:
{
"name": "learner-api-hosted",
"online": true,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": true,
"writePolicy": "ALLOW_ONCE"
}
}
If your local schema differs, follow the local schema and document
the difference. ALLOW_ONCE reinforces the chapter's
immutable-release mindset for this lab; it is not a universal
policy.
4. Create only when missing
repo_url="$NEXUS_URL/service/rest/v1/repositories/raw/hosted/learner-api-hosted"
if curl --silent --fail -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" "$repo_url" >/tmp/repo-current.json; then
echo 'Repository already exists; no POST performed.'
else
status=$(curl --silent --show-error -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" \
-H 'Content-Type: application/json' -o /tmp/repo-create.out -w '%{http_code}' \
-X POST "$NEXUS_URL/service/rest/v1/repositories/raw/hosted" --data-binary @repo.json)
case "$status" in 2*) echo "created ($status)" ;; *) cat /tmp/repo-create.out; exit 1 ;; esac
fi
The GET before POST is the key operation. A blind POST rerun models an action, not desired state.
5. Verify configuration and functional behavior
curl --fail --silent --show-error -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" "$repo_url" | python -m json.tool
printf 'chapter20 artifact\n' > /tmp/ch20-demo.txt
sha256sum /tmp/ch20-demo.txt
curl --fail --silent --show-error -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" --upload-file /tmp/ch20-demo.txt \
"$NEXUS_URL/repository/learner-api-hosted/learner-example/ch20/demo-1.0.0.txt"
curl --fail --silent --show-error -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" \
"$NEXUS_URL/repository/learner-api-hosted/learner-example/ch20/demo-1.0.0.txt" --output /tmp/ch20-downloaded.txt
sha256sum /tmp/ch20-downloaded.txt
The REST create changes repository configuration. Upload creates repository metadata plus blob content. Download verifies the data plane independently.
6. Update in place rather than delete-and-recreate
Read existing JSON, compare owned fields, and use the
format/type-specific PUT only if desired values differ. Toggling
online is a configuration change; deleting the
repository merely to change one field expands blast radius.
python - <<'PY'
import json
p=json.load(open('repo.json'))
p['online']=False
json.dump(p, open('/tmp/repo-offline.json','w'), indent=2)
PY
curl --fail --silent --show-error -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" \
-H 'Content-Type: application/json' -X PUT "$repo_url" --data-binary @/tmp/repo-offline.json
curl --fail --silent --show-error -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" "$repo_url" | python -m json.tool
curl --fail --silent --show-error -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" \
-H 'Content-Type: application/json' -X PUT "$repo_url" --data-binary @repo.json
7. Enumerate every asset page
Use the endpoint your local Swagger documents. The following pattern
handles any items/continuationToken
response correctly:
import os, requests
base = os.environ["NEXUS_URL"]
auth = (os.environ["NEXUS_ADMIN_USER"], os.environ["NEXUS_ADMIN_PASSWORD"])
url = f"{base}/service/rest/v1/assets"
base_params = {"repository": "learner-api-hosted"}
items, token = [], None
while True:
params = dict(base_params)
if token is not None:
params["continuationToken"] = token
r = requests.get(url, params=params, auth=auth, timeout=15)
r.raise_for_status()
body = r.json()
items.extend(body.get("items", []))
token = body.get("continuationToken")
if token is None:
break
print("complete_count=", len(items))
for item in items:
print(item.get("path"), item.get("downloadUrl"))
Do not log the auth tuple, Authorization headers, or full debug traces containing credentials.
8. Create a narrow role and local service user
Inventory current privilege IDs instead of inventing them:
curl --fail --silent --show-error -u "$NEXUS_ADMIN_USER:$NEXUS_ADMIN_PASSWORD" \
"$NEXUS_URL/service/rest/v1/security/privileges" > /tmp/privileges.json
python - <<'PY'
import json
for p in json.load(open('/tmp/privileges.json')):
name=p.get('name','')
if 'learner-api-hosted' in name:
print(name)
PY
Select only the exact browse/read/add privileges needed. A representative role payload is:
{
"id": "learner-api-publisher",
"name": "Chapter 20 disposable publisher",
"description": "Scope: learner-api-hosted only",
"privileges": [
"nx-repository-view-raw-learner-api-hosted-browse",
"nx-repository-view-raw-learner-api-hosted-read",
"nx-repository-view-raw-learner-api-hosted-add"
],
"roles": []
}
POST the role and local user using the request models in your
current Swagger. Inject the user password at runtime. Verify the
service user can upload/read the intended repository but receives
403 for administrative APIs.
9. A small idempotent reconciler
import os, requests
BASE = os.environ["NEXUS_URL"]
AUTH = (os.environ["NEXUS_ADMIN_USER"], os.environ["NEXUS_ADMIN_PASSWORD"])
NAME = "learner-api-hosted"
URL = f"{BASE}/service/rest/v1/repositories/raw/hosted/{NAME}"
CREATE_URL = f"{BASE}/service/rest/v1/repositories/raw/hosted"
DESIRED = {"name": NAME, "online": True, "storage": {
"blobStoreName": "default", "strictContentTypeValidation": True, "writePolicy": "ALLOW_ONCE"}}
def owned_view(obj):
s = obj.get("storage", {})
return {"name": obj.get("name"), "online": obj.get("online"),
"storage": {k: s.get(k) for k in DESIRED["storage"]}}
r = requests.get(URL, auth=AUTH, timeout=15)
if r.status_code == 404:
c = requests.post(CREATE_URL, json=DESIRED, auth=AUTH, timeout=15); c.raise_for_status(); print("changed=create")
else:
r.raise_for_status()
if owned_view(r.json()) == DESIRED:
print("changed=no")
else:
u = requests.put(URL, json=DESIRED, auth=AUTH, timeout=15); u.raise_for_status(); print("changed=update")
v = requests.get(URL, auth=AUTH, timeout=15); v.raise_for_status()
assert owned_view(v.json()) == DESIRED
print("verified=true")
Run twice. The first run may create/update; the second must report
changed=no unless drift occurred.
10. Challenge: choose the correct control
Requirement: developers may read and upload only to the disposable hosted repository, while provisioning must be rerunnable. Decide which state belongs in repository configuration, a role, the user, and the external secret store. Explain why delete/recreate is the wrong update strategy.
11. Cleanup and rollback
- Download and checksum the lab artifact one final time.
-
Delete only
svc-learner-apifrom the local lab realm. - Delete only
learner-api-publisher. -
Delete
learner-api-hostedonly after confirming its exact name and synthetic contents. - Verify all managed objects are absent.
- Unset secret environment variables and remove temporary API output.
Repository deletion is destructive. Do not generalize this sequence to a shared or production instance.
12. Knowledge check
Why query privileges after creating the repository?
To use exact current privilege IDs rather than guessing names from old documentation.
The second reconciler run sends another POST. What is missing?
Idempotent state detection: GET actual state and no-op when desired state already exists.
Why should the service user receive 403 on repository creation?
That negative test proves least privilege: data-plane access does not imply control-plane administration.
A list response contains many items and a token. Is it complete?
No. Continue until the returned token is null.
13. Summary and next step
You performed the full lifecycle: inspect, create, verify, update, paginate, build a narrow identity, reconcile twice, and clean up only managed resources. Lesson 3 turns these mechanics into design policy.
Official references and version notes
- Sonatype: Automation / REST API — OpenAPI/Swagger location, API integration, and Jetty 12 URL-encoding requirements.
- Sonatype: Nexus Repository API Reference — current endpoint catalog and request/response models.
- Sonatype: Repositories API — generic and format/type-specific repository configuration endpoints.
- Sonatype: Components API — component list/get/upload/delete behavior and continuation tokens.
- Sonatype: Pagination — continuation-token iteration contract.
- Sonatype: Security Management API — users, roles, privileges, content selectors, LDAP/SAML/OIDC boundaries.
- Sonatype: Authentication — local/external authentication and automation identity boundaries.
- Sonatype: User Tokens — current self-hosted Pro entitlement and token behavior.
- Sonatype: System Requirements — Java/database/storage baseline.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.