From e46ec46e15d2e03526a6c91aa676bf736373a7a3 Mon Sep 17 00:00:00 2001 From: Matthew Helmke Date: Mon, 20 Jul 2026 09:17:28 -0500 Subject: [PATCH 1/6] doc updates for apiv2 ga --- content/get-started/migration/_index.md | 7 +- content/platform/api/api-v2-migration.md | 248 +++++++++++++++++++++++ content/platform/api/api-v2-tutorial.md | 100 +++++---- 3 files changed, 308 insertions(+), 47 deletions(-) create mode 100644 content/platform/api/api-v2-migration.md diff --git a/content/get-started/migration/_index.md b/content/get-started/migration/_index.md index d72f59416e..bea8c1ea99 100644 --- a/content/get-started/migration/_index.md +++ b/content/get-started/migration/_index.md @@ -6,11 +6,14 @@ linktitle: "Migration" lead: "" type: "article" date: 2024-02-26T08:48:45+00:00 -lastmod: 2024-05-22T08:48:45+00:00 +lastmod: 2026-07-20T00:00:00+00:00 draft: false images: [] weight: 045 topic: true +crosslinks: +- title: "API v1 to v2 Migration" + url: "/platform/api/api-v2-migration/" banner: { image: "/icon-arrows_blurple.png", title: "Porting Applications to Chainguard", @@ -53,4 +56,4 @@ tutorials: [ }, ] ---- \ No newline at end of file +--- diff --git a/content/platform/api/api-v2-migration.md b/content/platform/api/api-v2-migration.md new file mode 100644 index 0000000000..d9ac63667a --- /dev/null +++ b/content/platform/api/api-v2-migration.md @@ -0,0 +1,248 @@ +--- +aliases: +- /chainguard/administration/api-v2-migration/ +title: "Migrating from API v1 to API v2" +linktitle: "API v1 to v2 Migration" +type: "article" +description: "How to migrate a direct integration with the Chainguard API from v1 to the GA API v2." +date: 2026-07-20T00:00:00+00:00 +lastmod: 2026-07-20T00:00:00+00:00 +draft: false +tags: ["Chainguard Console", "Procedural"] +images: [] +toc: true +weight: 040 +--- + +> **Draft status:** This page has several items marked `CONFIRM WITH ENGINEERING`. Do not publish externally until those are resolved. + +Chainguard's API v2 is now Generally Available (GA) across every product domain: Customer Platform (IAM), Containers, Ecosystems, and Integrations. At GA, the "beta" designation is dropped: endpoints move from `/v2beta1/` to `/v2/`. This guide covers what changed from v1, and the steps to migrate an existing direct integration. + +If you only use `chainctl`, the Chainguard Console, or the Terraform provider, you don't need this guide — those clients handle versioning for you, and nothing changes on your end. This guide is for anyone calling the API directly: `curl`, gRPC, or a custom SDK integration. + +v1 continues to work during a transition period, so nothing breaks today. Migrate at your own pace; see [Timeline](#timeline) for what happens next. + +## Prerequisites + +You'll need: + +- A valid Chainguard identity with access to the resources you're calling. +- `chainctl` installed, to mint a token for testing (`chainctl auth token`). + +Set up your environment for the following examples: + +```shell +export TOKEN=$(chainctl auth token) +export API=https://console-api.enforce.dev +# ORG_ID is the UID of your root organization group +export ORG_ID=YOUR_ORG_ID +``` + +## What's not changing + +- **Authentication** — same OIDC token model as v1. +- **Authorization** — same identity-based access control. +- **Scoping** — same `uidp.descendants_of` / `uidp.children_of` hierarchy filters. +- **Host** — same API host (`console-api.enforce.dev` in production). + +If your integration already authenticates against v1 successfully, no credential or auth-flow changes are needed to call v2. + +## Quick reference + +| Operation | v1 pattern | v2 pattern | +| --- | --- | --- | +| Path versioning | Unversioned (`/iam/...`, `/registry/...`, `/vulnerabilities/...`, `/libraries/...`, `/advisory/...`) | Versioned: `/v2/...` for every domain | +| Field naming | Inconsistent (`id`, `created_at`, `group_id`) | Consistent camelCase (`uid`, `createTime`/`updateTime`) | +| CREATE | Parent resource identified in the URL path | Parent identified in the request body | +| GET (single resource) | No dedicated endpoint; filter a List call | Dedicated Get endpoint, fetch by `uid` | +| LIST | Offset/page-number params, full result set, no total count | `page_size`, `page_token`, `skip`, `order_by`, `total_count` | +| UPDATE | PUT, full-resource replacement | PATCH, partial update via a field mask | +| DELETE | Same path pattern, ambiguous failure on blocked deletes | Same path pattern, returns a `PreconditionFailure` detail when blocked | +| Errors | Ambiguous — a missing resource can return an empty HTTP 200 result | Structured: `ErrorInfo`, `BadRequest`, `ResourceInfo`, `PreconditionFailure`, with proper status codes | +| Rate limiting | Not enforced | Enforced | + +### Path prefixes by domain + +| Domain | v1 path prefix | v2 path prefix (GA) | +| --- | --- | --- | +| IAM (Customer Platform: groups, roles, identities, role bindings) | `/iam/` | `/iam/v2/` | +| Registry (Containers) | `/registry/` | `/registry/v2/` | +| Vulnerabilities | `/vulnerabilities/` | `/vulnerabilities/v2/` | +| Libraries (Ecosystems) | `/libraries/` | `/libraries/v2/` | +| Advisory (Integrations) | `/advisory/` | `/advisory/v2/` | + +If you built against the beta endpoints, updating the version segment from `v2beta1` to `v2` is the only path change — the request and response shapes don't change as part of that rename. + +## Migration steps + +### Step 1: Update your endpoint paths + +Add the `v2` version segment for each domain you call, using the [path prefix table](#path-prefixes-by-domain). If you were already on `/v2beta1/`, rename it to `/v2/`. + +### Step 2: Update field references + +v2 standardizes field naming across every service: + +- Resource identifiers are always `uid`, never `id`. +- Timestamps are `createTime` / `updateTime`, not `created_at` / `updated_at`. +- All fields use camelCase. + +Update any code that reads specific field names out of API responses — these differ from v1 even for the same resource. + +### Step 3: Move CREATE calls to parent-in-body + +v1 identifies the parent resource in the URL path (for example, `/groups/{parent}`). v2 moves the parent identifier into the request body instead, alongside the fields of the resource being created. + +**v1 (illustrative):** + +```shell +curl -X POST -H "Authorization: Bearer $TOKEN" \ + "$API/iam/groups/$PARENT_UID" \ + -d '{"name": "my-group"}' +``` + +**v2 (illustrative):** + +```shell +curl -X POST -H "Authorization: Bearer $TOKEN" \ + "$API/iam/v2/groups" \ + -d '{"parent": "'"$PARENT_UID"'", "group": {"name": "my-group"}}' +``` + +> **CONFIRM WITH ENGINEERING:** this request body is illustrative. Get the exact body schema — including the `"group"` wrapper field name — from engineering before publishing. + +Stop building create-call URLs with the parent embedded in the path. Build a request body that includes both the parent identifier and the new resource's fields instead. + +### Step 4: Switch updates to PATCH with a field mask + +v1 uses PUT semantics: changing one field means sending the entire resource back, which forces a fetch-before-write pattern and creates race conditions on concurrent updates. v2 uses PATCH with a field mask, so you specify only the fields you're changing. + +```shell +curl -X PATCH -H "Authorization: Bearer $TOKEN" \ + "$API/iam/v2/groups/$GROUP_UID?updateMask=description" \ + -d '{"description": "updated description"}' +``` + +Testing against the live beta API confirmed this `updateMask` parameter name works. It's presumed unchanged at GA, but worth a final confirmation since none of the GA source docs restate it. + +Replace fetch-modify-write update logic with a PATCH call that sets only the fields you're changing. A wildcard `*` is available if you genuinely want full replacement. + +### Step 5: Switch pagination to cursor-based + +v1 pagination relies on page numbers or offsets, and loads the full result set into memory. v2 List endpoints add: + +- `page_size` — how many items to return per page. +- `page_token` — the opaque cursor from the previous response's `next_page_token`, used to fetch the next page. Omit it for the first page. +- `skip` — number of items to skip, for jumping directly to a specific page without walking the cursor sequentially. +- `order_by` — server-side ordering, instead of sorting client-side after fetching. +- `total_count` — total matching items, so you don't have to infer it from page math. + +```shell +curl -H "Authorization: Bearer $TOKEN" \ + "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=3&order_by=name" | jq . +``` + +Replace any page-number or offset logic with a loop that follows `page_token` until the response stops returning one. Use `skip` instead if you need to jump to an arbitrary page. Adopt `order_by` and drop any client-side sort you were doing to compensate. + +### Step 6: Use dedicated Get endpoints + +v1 typically requires filtering a List call to fetch a single resource. v2 adds a dedicated Get endpoint per resource, fetched by `uid` directly. + +```shell +curl -H "Authorization: Bearer $TOKEN" \ + "$API/iam/v2/groups/$GROUP_UID" +``` + +Replace any "list and filter to one item" pattern with the dedicated Get endpoint for that resource. + +### Step 7: Update your error handling + +In v1, an empty or ambiguous result can mean several different things — no matches, no permission, or a missing parent resource — with no way for a client to tell them apart. For example, requesting a resource that doesn't exist in v1 returns an empty result with HTTP 200. + +v2 returns proper status codes (for example, `NOT_FOUND` / HTTP 404) with structured error details, following the same error model used across Google Cloud APIs: + +- **ErrorInfo** — a machine-readable reason code and error domain. +- **BadRequest** — per-field validation errors. +- **ResourceInfo** — which specific resource type and name wasn't found. +- **PreconditionFailure** — why a request was blocked (for example, a delete blocked by a dependent resource). + +If your integration currently treats an empty v1 result as ambiguous, or works around that ambiguity with extra calls, update it to branch on the v2 status code and structured error details instead. Re-test your error-handling paths, not just the happy path. + +### Step 8: Review rate limits + +Rate limits were not enforced during beta. They are enforced starting at GA. + +> **CONFIRM WITH ENGINEERING:** specific rate limits and response headers (for example, remaining-quota headers). Add them here once confirmed, and link out to a rate-limits reference page if one exists. + +Review your call patterns — polling frequency, batch sizes — against the confirmed limits before cutting production traffic over. + +### Step 9: Update gRPC and SDK clients + +All v2 endpoints are also available directly via gRPC, at the same host. Proto definitions are at `chainguard.dev/sdk/proto/chainguard/platform/`. If you call the API via generated gRPC clients, regenerate them against the GA `v2` proto packages, renamed from `v2beta1`. + +> **CONFIRM WITH ENGINEERING:** whether the Go SDK client library actually moves from `chainguard.dev/sdk/proto/platform/clients/v2beta1` to an equivalent `v2` path at GA, or keeps the `v2beta1` package name internally even after the REST paths rename. + +## Full example: listing and deleting groups + +**Before (v1):** + +```shell +curl -H "Authorization: Bearer $TOKEN" \ + "$API/iam/groups?uidp.descendants_of=$ORG_ID" +``` + +**After (v2):** + +```shell +curl -H "Authorization: Bearer $TOKEN" \ + "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=3&order_by=name" | jq . +``` + +Delete calls follow the same path-prefix change, with no other behavior change: + +```shell +curl -X DELETE -H "Authorization: Bearer $TOKEN" \ + "$API/iam/v2/roleBindings/$BINDING_UID" +curl -X DELETE -H "Authorization: Bearer $TOKEN" \ + "$API/iam/v2/identities/$IDENTITY_UID" +curl -X DELETE -H "Authorization: Bearer $TOKEN" \ + "$API/iam/v2/groups/$GROUP_UID" +``` + +Each `DELETE` returns an empty response body on success, same as v1. A delete blocked by a dependent resource now returns a `PreconditionFailure` error detail explaining why, instead of an ambiguous failure. + +## Timeline + +| Phase | When | What it means for you | +| --- | --- | --- | +| Parallel availability | GA onward | Both v1 and v2 serve traffic. v1 responses include deprecation headers noting the eventual sunset date. Migrate at your own pace. | +| Warning escalation | `CONFIRM WITH ENGINEERING` | v1 responses add an additional warning header. If you have meaningful v1 traffic, your Chainguard account team may reach out directly. | +| Soft shutdown | `CONFIRM WITH ENGINEERING` | v1 requests return an error with migration guidance, for a short grace period. | +| Hard removal | `CONFIRM WITH ENGINEERING` | v1 is fully removed. Requests to v1 endpoints fail. | + +> **CONFIRM WITH ENGINEERING:** every date in this timeline is a placeholder. Don't ship this table as-is. + +Don't wait for a hard deadline to start testing v2 — both versions are available today, and migration can happen incrementally. + +## Troubleshooting + +**Will my existing integration stop working today?** +No. v1 keeps working during the parallel availability period. Nothing changes for existing integrations until the sunset date, which will be communicated well in advance. + +**Do I need new credentials for v2?** +No. Authentication and authorization are unchanged — the same tokens and identities that work with v1 work with v2. + +**Why did an API call that used to return an empty list now return an error?** +This is expected, and it's an improvement. In v1, a missing resource and an empty result look the same, so there was no way to tell "nothing matched" apart from "that resource doesn't exist" or "you don't have permission." v2 returns a specific, structured error in those cases instead. + +**Why did my create call stop working?** +Check whether you're building the request URL with the parent resource in the path — that's the v1 pattern. In v2, the parent goes in the request body instead. See [Step 3](#step-3-move-create-calls-to-parent-in-body). + +**Where do I send a customer who asks about migrating?** +This guide. If you're on the account or support team, reach out to Engineering for anything marked `CONFIRM WITH ENGINEERING` in this guide rather than guessing at specifics. + +## Next steps + +- [Chainguard API v2 Tutorial](/platform/api/api-v2-tutorial/) — worked examples of the v2 endpoints you'll be migrating to. +- [Chainguard API v2 Specification](/platform/api/spec-api-v2/) — full OpenAPI reference. +- Reach out to your Chainguard account team or [Chainguard Support](https://support.chainguard.dev/) if you run into issues migrating. diff --git a/content/platform/api/api-v2-tutorial.md b/content/platform/api/api-v2-tutorial.md index a0c0b97c76..9d48bc256d 100644 --- a/content/platform/api/api-v2-tutorial.md +++ b/content/platform/api/api-v2-tutorial.md @@ -6,7 +6,7 @@ linktitle: "API v2 Tutorial" type: "article" description: "Tutorial with examples showing how you can use the Chainguard API v2." date: 2026-03-30T08:49:31+00:00 -lastmod: 2026-04-02T00:00:01+01:00 +lastmod: 2026-07-20T00:00:00+00:00 draft: false tags: ["Chainguard Console", "Procedural"] images: [] @@ -14,11 +14,11 @@ toc: true weight: 030 --- -{{< beta feature="The Chainguard API v2" >}} +> **Draft status:** This page has several items marked `CONFIRM WITH ENGINEERING`. Do not publish externally until those are resolved. -The v2 API introduces cursor-based pagination, server-side ordering, consistent resource patterns, and structured error responses across all endpoints. +The v2 API is now Generally Available (GA) and introduces cursor-based pagination, server-side ordering, consistent resource patterns, and structured error responses across all endpoints. -This guide walks through the v2 API using real `curl` commands. +This guide walks through the v2 API using real `curl` commands. If you're migrating an existing v1 or beta (`v2beta1`) integration, see [Migrating from API v1 to API v2](/platform/api/api-v2-migration/) instead. > **Note:** The example output in this guide was captured from a development environment. Your organization's resource names, UIDs, timestamps, and counts will differ. The response structure and field names are the same across all environments. @@ -36,7 +36,7 @@ This guide walks through the v2 API using real `curl` commands. - **Structured errors** with typed detail payloads (AIP-193) - **Consistent resource patterns** — every resource has `uid`, `createTime`, `updateTime` - **Hydrated references** — role binding responses include full identity, group, and role objects -- **FieldMask updates** — partial updates via `update_mask` instead of sending the full resource +- **FieldMask updates** — partial updates via `updateMask` instead of sending the full resource ## Available endpoints @@ -45,8 +45,12 @@ This guide walks through the v2 API using real `curl` commands. | **IAM** | Groups, Identities, Roles, RoleBindings, IdentityProviders, AccountAssociations, GroupInvites | List, Get, Create, Update, Delete | | **Registry** | Repos, Tags | List, Get | | **Vulnerabilities** | Advisories | List, Get | +| **Ecosystems (Libraries)** | — | — | +| **Integrations (Advisory)** | — | — | -All endpoints live under `/iam/v2beta1/`, `/registry/v2beta1/`, or `/vulnerabilities/v2beta1/`. +All endpoints live under a versioned path per domain: `/iam/v2/`, `/registry/v2/`, `/vulnerabilities/v2/`, `/libraries/v2/`, or `/advisory/v2/`. + +> **CONFIRM WITH ENGINEERING:** Ecosystems and Integrations are new domains at GA (beta only covered IAM, Registry, and Vulnerabilities). Get the specific resource names and supported operations for these two domains before publishing — the worked examples in this guide only cover the three domains available during beta. ## Prerequisites @@ -59,16 +63,18 @@ export API=https://console-api.enforce.dev export ORG_ID=YOUR_ORG_ID ``` -All examples below use `$TOKEN`, `$API`, and `$ORG_ID` for brevity. +The following examples use `$TOKEN`, `$API`, and `$ORG_ID` for brevity. -## Beta notes +## Operational notes Keep the following in mind as you work through this guide. - **Page tokens expire after 3 days** ([AIP-158](https://google.aip.dev/158)). If a token expires, the query restarts from the beginning — no error is returned. -- **Rate limits** are not enforced during beta. They will be introduced at GA. +- **Rate limits** are enforced starting at GA. - **gRPC** — all endpoints are also available via gRPC at the same host. Proto definitions are at `chainguard.dev/sdk/proto/chainguard/platform/`. +> **CONFIRM WITH ENGINEERING:** specific rate limits and response headers, and whether the Go SDK client library moves from `chainguard.dev/sdk/proto/platform/clients/v2beta1` to a `v2` path at GA. Don't publish this section externally until both are confirmed. + --- ## 1. Your first v2 request @@ -77,7 +83,7 @@ List the first 3 groups in your organization: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&page_size=3&order_by=name" | jq . + "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=3&order_by=name" | jq . ``` ```json @@ -130,7 +136,7 @@ New in v2: fetch a resource directly by UID. In v1, this required a List call wi ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2beta1/groups/$GROUP_UID" | jq '{uid, name, description}' + "$API/iam/v2/groups/$GROUP_UID" | jq '{uid, name, description}' ``` ```json @@ -149,7 +155,7 @@ Find a specific group without knowing its UID: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&name=platform" \ + "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&name=platform" \ | jq '[.groups[] | {uid, name, description}]' ``` @@ -176,8 +182,8 @@ A real workflow: create an org folder, add an identity, and bind a role. ```shell curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2beta1/groups/$ORG_ID" \ - -d '{"name": "backend-team", "description": "Backend engineering team"}' | jq . + "$API/iam/v2/groups" \ + -d '{"parent": "'"$ORG_ID"'", "group": {"name": "backend-team", "description": "Backend engineering team"}}' | jq . ``` ```json @@ -192,7 +198,9 @@ curl -s -X POST -H "Authorization: Bearer $TOKEN" \ } ``` -> **Note:** The parent group goes in the URL path. The request body contains only the resource fields. +> **Note:** The parent group goes in the request body, not the URL path — a change from beta. See [Migrating from API v1 to API v2](/platform/api/api-v2-migration/#step-3-move-create-calls-to-parent-in-body) for details. +> +> **CONFIRM WITH ENGINEERING:** this request body shape, including the `"group"` wrapper field name, is illustrative. Confirm the exact schema before publishing. ### Create an identity @@ -202,17 +210,22 @@ export GROUP_UID=YOUR_GROUP_UID curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2beta1/identities/$GROUP_UID" \ + "$API/iam/v2/identities" \ -d '{ - "name": "ci-bot", - "description": "CI/CD pipeline identity", - "claimMatch": { - "issuer": "https://token.actions.githubusercontent.com", - "subject": "repo:my-org/my-repo:ref:refs/heads/main" + "parent": "'"$GROUP_UID"'", + "identity": { + "name": "ci-bot", + "description": "CI/CD pipeline identity", + "claimMatch": { + "issuer": "https://token.actions.githubusercontent.com", + "subject": "repo:my-org/my-repo:ref:refs/heads/main" + } } }' | jq . ``` +> **CONFIRM WITH ENGINEERING:** this request body shape, including the `"identity"` wrapper field name, is illustrative. Confirm the exact schema before publishing. + ```json { "uid": "d9e2f1a0.../fb139588d99c8efe/f462d354ca32ca9f", @@ -236,7 +249,7 @@ First, find the viewer role: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2beta1/roles" | jq '.roles[] | select(.name == "viewer") | {uid, name, description}' + "$API/iam/v2/roles" | jq '.roles[] | select(.name == "viewer") | {uid, name, description}' ``` ```json @@ -257,10 +270,12 @@ export IDENTITY_UID=YOUR_IDENTITY_UID curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2beta1/roleBindings/$GROUP_UID" \ - -d "{\"identityUid\": \"$IDENTITY_UID\", \"roleUid\": \"$ROLE_UID\"}" | jq . + "$API/iam/v2/roleBindings" \ + -d "{\"parent\": \"$GROUP_UID\", \"roleBinding\": {\"identityUid\": \"$IDENTITY_UID\", \"roleUid\": \"$ROLE_UID\"}}" | jq . ``` +> **CONFIRM WITH ENGINEERING:** this request body shape, including the `"roleBinding"` wrapper field name, is illustrative. Confirm the exact schema before publishing. + ```json { "uid": "d9e2f1a0.../fb139588.../9b822036a7075d75", @@ -297,7 +312,7 @@ Every List endpoint supports cursor-based pagination with consistent parameters. ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&page_size=5" \ + "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5" \ | jq '{totalCount, groups: [.groups[].name], nextPageToken: .nextPageToken[:20]}' ``` @@ -313,7 +328,7 @@ Follow the cursor for the next page: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&page_size=5&page_token=CqQBV3lK..." \ + "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5&page_token=CqQBV3lK..." \ | jq '{groups: [.groups[].name]}' ``` @@ -331,7 +346,7 @@ Sort by name: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name" \ + "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name" \ | jq '[.groups[].name]' ``` @@ -343,7 +358,7 @@ Reverse the order: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name%20desc" \ + "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name%20desc" \ | jq '[.groups[].name]' ``` @@ -355,7 +370,7 @@ Sort by creation time (newest first): ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=created_at%20desc" \ + "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=created_at%20desc" \ | jq '[.groups[] | {name, createTime}]' ``` @@ -377,7 +392,7 @@ Jump directly to page 3 (skip the first 10 results): ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2beta1/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name&skip=10" \ + "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name&skip=10" \ | jq '{skipped: .skipped, groups: [.groups[].name]}' ``` @@ -396,7 +411,7 @@ The `skipped` field in the response confirms how many results were skipped, usef | ----------- | ------------- | | `page_size` | Number of results per page (default 50, max 200) | | `page_token` | Opaque cursor from previous response's `nextPageToken` | -| `order_by` | Sort field and direction, e.g. `name`, `created_at desc` | +| `order_by` | Sort field and direction, for example `name` or `created_at desc` | | `skip` | Number of results to skip (for random-access / UI page jumping) | --- @@ -407,7 +422,7 @@ List repos scoped to your organization: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/registry/v2beta1/repos?uidp.descendants_of=$ORG_ID&page_size=3" \ + "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&page_size=3" \ | jq '[.repos[] | {uid, name, createTime}]' ``` @@ -432,8 +447,8 @@ API v2 returns structured error responses with machine-parseable codes and detai ```shell curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2beta1/groups/$ORG_ID" \ - -d '{}' | jq . + "$API/iam/v2/groups" \ + -d '{"parent": "'"$ORG_ID"'", "group": {}}' | jq . ``` ```json @@ -499,7 +514,7 @@ Update specific fields without sending the full resource. Only the fields listed ```shell curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2beta1/groups/$GROUP_UID" \ + "$API/iam/v2/groups/$GROUP_UID" \ -d '{ "description": "Updated description — only this field changes" }' | jq '{uid, name, description}' @@ -520,7 +535,7 @@ To be explicit about which fields to update, pass `updateMask`: ```shell curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2beta1/groups/$GROUP_UID?updateMask=description" \ + "$API/iam/v2/groups/$GROUP_UID?updateMask=description" \ -d '{ "description": "Only this field is updated", "name": "this-is-ignored" @@ -541,12 +556,7 @@ The `name` in the body is ignored because `updateMask` only includes `descriptio ## Migration from v1 -v2 is additive — v1 endpoints remain available. You can migrate at your own pace: - -- Replace `/iam/v1/` with `/iam/v2beta1/` in your API calls -- Update field names: `id` → `uid`, `createdAt` → `createTime`, `updatedAt` → `updateTime` -- Add pagination handling for List endpoints (or set `page_size` high for small result sets) -- v1 will have a deprecation window after v2 reaches GA +v2 is additive — v1 endpoints remain available during a transition period, so you can migrate at your own pace. For the full field-by-field mapping and migration timeline, see [Migrating from API v1 to API v2](/platform/api/api-v2-migration/). --- @@ -557,9 +567,9 @@ Delete resources you created during this walkthrough: ```shell # Delete in reverse order: role binding, identity, group # BINDING_UID is the uid value returned in the Bind a role response above -curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$API/iam/v2beta1/roleBindings/$BINDING_UID" -curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$API/iam/v2beta1/identities/$IDENTITY_UID" -curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$API/iam/v2beta1/groups/$GROUP_UID" +curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$API/iam/v2/roleBindings/$BINDING_UID" +curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$API/iam/v2/identities/$IDENTITY_UID" +curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$API/iam/v2/groups/$GROUP_UID" ``` Each DELETE returns an empty response body on success. From 2a2dfdb88445793f9a0287f636aa9d8d245150b1 Mon Sep 17 00:00:00 2001 From: Matthew Helmke Date: Tue, 21 Jul 2026 09:06:05 -0500 Subject: [PATCH 2/6] docs: cover top direct-API operations in v2 tutorial Add worked examples for the operations that direct API integrations (gRPC and curl) use most, based on v1 usage data: registry repo Get and Update, tag listing and the deprecated flag, and vulnerability advisories. Cross-reference the repo PATCH example from the migration guide's field-mask step. Align new examples with the published v2beta1 spec (paths, response fields, tag deprecation, advisory fields) and mark the remaining GA divergences CONFIRM WITH ENGINEERING: the missing vulnerability-report endpoints and the absence of a server-side end-of-life tag filter. Co-Authored-By: Claude Opus 4.8 --- content/platform/api/api-v2-migration.md | 2 + content/platform/api/api-v2-tutorial.md | 111 ++++++++++++++++++++++- 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/content/platform/api/api-v2-migration.md b/content/platform/api/api-v2-migration.md index d9ac63667a..d9fc45ac09 100644 --- a/content/platform/api/api-v2-migration.md +++ b/content/platform/api/api-v2-migration.md @@ -127,6 +127,8 @@ Testing against the live beta API confirmed this `updateMask` parameter name wor Replace fetch-modify-write update logic with a PATCH call that sets only the fields you're changing. A wildcard `*` is available if you genuinely want full replacement. +Repository updates (`UpdateRepo`) are a common case to migrate here. For a worked repo PATCH example, see [Partial updates with FieldMask](/platform/api/api-v2-tutorial/#7-partial-updates-with-fieldmask) in the tutorial. + ### Step 5: Switch pagination to cursor-based v1 pagination relies on page numbers or offsets, and loads the full result set into memory. v2 List endpoints add: diff --git a/content/platform/api/api-v2-tutorial.md b/content/platform/api/api-v2-tutorial.md index 9d48bc256d..76a389b7ba 100644 --- a/content/platform/api/api-v2-tutorial.md +++ b/content/platform/api/api-v2-tutorial.md @@ -418,6 +418,10 @@ The `skipped` field in the response confirms how many results were skipped, usef ## 4. Querying the registry +Registry endpoints follow the same List, Get, and pagination patterns as the IAM examples earlier, applied to repos and tags. If your integration reads image metadata — listing repos, walking tags, or checking end-of-life status — these are the endpoints you call most. + +### List repos + List repos scoped to your organization: ```shell @@ -434,11 +438,91 @@ curl -s -H "Authorization: Bearer $TOKEN" \ ] ``` -Same pagination and ordering parameters work on all List endpoints. +The same pagination and ordering parameters work on every List endpoint. + +### Get a single repo + +Fetch one repo directly by UID, the same way you fetch a group: + +```shell +# REPO_UID is a uid value from the List repos response above +export REPO_UID=YOUR_REPO_UID + +curl -s -H "Authorization: Bearer $TOKEN" \ + "$API/registry/v2/repos/$REPO_UID" | jq '{uid, name, createTime}' +``` + +```json +{ + "uid": "d9e2f1a0.../06626efd8c6b3fb7", + "name": "nginx", + "createTime": "2026-01-28T12:54:21.189Z" +} +``` + +### List tags in a repo + +Scope a tag listing to a single repo with `uidp.children_of`. Each tag carries its `digest` and a `deprecated` flag: + +```shell +curl -s -H "Authorization: Bearer $TOKEN" \ + "$API/registry/v2/tags?uidp.children_of=$REPO_UID&page_size=3" \ + | jq '[.tags[] | {name, digest, deprecated, updateTime}]' +``` + +```json +[ + {"name": "latest", "digest": "sha256:6b3f...", "deprecated": false, "updateTime": "2026-07-14T09:12:44.501Z"}, + {"name": "1.27", "digest": "sha256:8c1a...", "deprecated": false, "updateTime": "2026-07-14T09:12:44.502Z"}, + {"name": "1.26", "digest": "sha256:a90d...", "deprecated": true, "updateTime": "2026-05-02T18:30:10.114Z"} +] +``` + +### Check for deprecated tags + +In v1, a dedicated `ListEolTags` call surfaced end-of-life tags. v2beta1 has no separate end-of-life endpoint or server-side filter. Instead, each tag carries a `deprecated` boolean, which you filter on client-side: + +```shell +curl -s -H "Authorization: Bearer $TOKEN" \ + "$API/registry/v2/tags?uidp.children_of=$REPO_UID&page_size=200" \ + | jq '[.tags[] | select(.deprecated) | .name]' +``` + +> **CONFIRM WITH ENGINEERING:** v2beta1 exposes tag deprecation only as a per-tag `deprecated` field, with no server-side end-of-life filter. Confirm whether GA adds a dedicated filter or endpoint, since v1's `ListEolTags` was a common direct-API call. --- -## 5. Structured errors +## 5. Querying vulnerabilities + +The Vulnerabilities domain exposes advisory data. In v2beta1 it covers advisories with List and Get. + +### List advisories + +Advisories are scoped and paginated like every other List endpoint, with extra filters such as `artifactNames` and `advisoryIds`: + +```shell +curl -s -H "Authorization: Bearer $TOKEN" \ + "$API/vulnerabilities/v2/advisories?uidp.descendants_of=$ORG_ID&page_size=3" \ + | jq '[.advisories[] | {uid, advisoryId, artifactName, updateTime}]' +``` + +```json +[ + {"uid": "d9e2f1a0.../3b1c", "advisoryId": "CGA-abcd-1234-wxyz", "artifactName": "nginx", "updateTime": "2026-07-18T21:04:11.220Z"}, + {"uid": "d9e2f1a0.../7f2a", "advisoryId": "CGA-efgh-5678-stuv", "artifactName": "python", "updateTime": "2026-07-18T21:04:11.221Z"}, + {"uid": "d9e2f1a0.../a1b2", "advisoryId": "CGA-ijkl-9012-mnop", "artifactName": "openssl", "updateTime": "2026-07-18T21:04:11.222Z"} +] +``` + +Fetch a single advisory by UID at `/vulnerabilities/v2/advisories/{uid}`. + +### Vulnerability reports + +> **CONFIRM WITH ENGINEERING:** the v1 `GetVulnReport` and `ListVulnCountReports` calls — used heavily by direct HTTP integrations — have no equivalent in the v2beta1 spec, which exposes only advisories. Confirm whether GA adds vulnerability-report endpoints, or whether advisory data is meant to replace them. This is a real migration gap for the CI/CD audience that reads scan results programmatically. + +--- + +## 6. Structured errors API v2 returns structured error responses with machine-parseable codes and details. @@ -507,7 +591,7 @@ Error responses follow [Google AIP-193](https://google.aip.dev/193) with typed d --- -## 6. Partial updates with FieldMask +## 7. Partial updates with FieldMask Update specific fields without sending the full resource. Only the fields listed in `updateMask` are changed: @@ -552,6 +636,27 @@ curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \ The `name` in the body is ignored because `updateMask` only includes `description`. +### Update a repo + +The same PATCH-with-field-mask pattern applies to every updatable resource. Repository updates are a common case to migrate from v1, where changing one field meant sending the full resource: + +```shell +curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + "$API/registry/v2/repos/$REPO_UID?updateMask=description" \ + -d '{"description": "Updated repo description"}' | jq '{uid, name, description}' +``` + +```json +{ + "uid": "d9e2f1a0.../06626efd8c6b3fb7", + "name": "nginx", + "description": "Updated repo description" +} +``` + +> **Note:** The repo update path (`/registry/v2/repos/{repo.uid}`) and request body are confirmed against the published v2beta1 spec, where `name` and `description` are writable. As with the group example, `updateMask` is accepted by the live API but isn't restated in the spec, so reconfirm it holds at GA. + --- ## Migration from v1 From 9df40f5e08bd1b654b4c3c97b2e87267f44a5e08 Mon Sep 17 00:00:00 2001 From: Matthew Helmke Date: Tue, 21 Jul 2026 10:15:04 -0500 Subject: [PATCH 3/6] Use GitHub immutable subject claims in identity examples Update the OIDC subject claim in the identity and role-binding examples to GitHub's immutable format (repo:org@/repo@:...). This keeps the tutorial consistent with the broader docs update in chainguard-dev/edu#3616, which explains the format and how to find the numeric IDs. Refs: DOCS-73 Co-Authored-By: Claude Opus 4.8 --- content/platform/api/api-v2-tutorial.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/content/platform/api/api-v2-tutorial.md b/content/platform/api/api-v2-tutorial.md index 76a389b7ba..dbc58fb5ce 100644 --- a/content/platform/api/api-v2-tutorial.md +++ b/content/platform/api/api-v2-tutorial.md @@ -218,7 +218,7 @@ curl -s -X POST -H "Authorization: Bearer $TOKEN" \ "description": "CI/CD pipeline identity", "claimMatch": { "issuer": "https://token.actions.githubusercontent.com", - "subject": "repo:my-org/my-repo:ref:refs/heads/main" + "subject": "repo:my-org@123456/my-repo@654321:ref:refs/heads/main" } } }' | jq . @@ -236,11 +236,15 @@ curl -s -X POST -H "Authorization: Bearer $TOKEN" \ "updateTime": "2026-03-27T13:55:00.785Z", "claimMatch": { "issuer": "https://token.actions.githubusercontent.com", - "subject": "repo:my-org/my-repo:ref:refs/heads/main" + "subject": "repo:my-org@123456/my-repo@654321:ref:refs/heads/main" } } ``` +{{< note >}} +The `subject` shown here uses GitHub's immutable format, which embeds the numeric owner ID (`123456`) and repository ID (`654321`). Match the exact subject your repository's token carries. For how to find these IDs and when the format applies, see [Create an Assumable Identity for a GitHub Actions Workflow](/platform/administration/assumable-ids/identity-examples/github-identity/#finding-your-repositorys-numeric-identifiers). +{{< /note >}} + Note the identity `uid` in the response — you will use it in the next step when binding a role. ### Bind a role @@ -283,7 +287,7 @@ curl -s -X POST -H "Authorization: Bearer $TOKEN" \ "uid": "d9e2f1a0.../fb139588.../f462d354ca32ca9f", "name": "ci-bot", "description": "CI/CD pipeline identity", - "subject": "repo:my-org/my-repo:ref:refs/heads/main", + "subject": "repo:my-org@123456/my-repo@654321:ref:refs/heads/main", "issuer": "https://token.actions.githubusercontent.com" }, "group": { From baae186f0e7c112b096d54f299610517b908cf08 Mon Sep 17 00:00:00 2001 From: Matthew Helmke Date: Tue, 4 Aug 2026 08:01:07 -0500 Subject: [PATCH 4/6] docs: apply engineering review to API v2 GA docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve the CONFIRM WITH ENGINEERING items from the PR review: - CREATE is unchanged from v1 — parent stays in the URL path, no wrapper. Revert the tutorial creates and validation example, drop the migration CREATE step, and fold a "no change" note into What's not changing. - Remove rate-limit content; rate limiting is not part of this release. - Replace the migration timeline table with prose: no sunset dates or deprecation headers ship now, and v1 stays fully supported. - Fill in the Available endpoints table (Ecosystems, Integrations, Events); none are new at GA. Correct v1 paths as versioned. - Settle tag end-of-life (client-side deprecated filter; v1 ListEolTags remains) and vulnerability-report guidance (continue on v1). - Set the Go SDK clients path to clients/v2 and sweep stray v2beta1 references. - Remove the draft banners. Param casing and updateMask remain open but non-blocking. Co-Authored-By: Claude Opus 4.8 --- content/platform/api/api-v2-migration.md | 88 ++++++------------------ content/platform/api/api-v2-tutorial.md | 67 ++++++++---------- 2 files changed, 47 insertions(+), 108 deletions(-) diff --git a/content/platform/api/api-v2-migration.md b/content/platform/api/api-v2-migration.md index d9fc45ac09..e37af18576 100644 --- a/content/platform/api/api-v2-migration.md +++ b/content/platform/api/api-v2-migration.md @@ -6,7 +6,7 @@ linktitle: "API v1 to v2 Migration" type: "article" description: "How to migrate a direct integration with the Chainguard API from v1 to the GA API v2." date: 2026-07-20T00:00:00+00:00 -lastmod: 2026-07-20T00:00:00+00:00 +lastmod: 2026-08-04T13:01:07+00:00 draft: false tags: ["Chainguard Console", "Procedural"] images: [] @@ -14,8 +14,6 @@ toc: true weight: 040 --- -> **Draft status:** This page has several items marked `CONFIRM WITH ENGINEERING`. Do not publish externally until those are resolved. - Chainguard's API v2 is now Generally Available (GA) across every product domain: Customer Platform (IAM), Containers, Ecosystems, and Integrations. At GA, the "beta" designation is dropped: endpoints move from `/v2beta1/` to `/v2/`. This guide covers what changed from v1, and the steps to migrate an existing direct integration. If you only use `chainctl`, the Chainguard Console, or the Terraform provider, you don't need this guide — those clients handle versioning for you, and nothing changes on your end. This guide is for anyone calling the API directly: `curl`, gRPC, or a custom SDK integration. @@ -44,6 +42,7 @@ export ORG_ID=YOUR_ORG_ID - **Authorization** — same identity-based access control. - **Scoping** — same `uidp.descendants_of` / `uidp.children_of` hierarchy filters. - **Host** — same API host (`console-api.enforce.dev` in production). +- **CREATE shape** — the parent resource stays in the URL path (`/groups/{parent}`), and the request body carries the resource fields directly. This is unchanged from v1. If your integration already authenticates against v1 successfully, no credential or auth-flow changes are needed to call v2. @@ -51,25 +50,24 @@ If your integration already authenticates against v1 successfully, no credential | Operation | v1 pattern | v2 pattern | | --- | --- | --- | -| Path versioning | Unversioned (`/iam/...`, `/registry/...`, `/vulnerabilities/...`, `/libraries/...`, `/advisory/...`) | Versioned: `/v2/...` for every domain | +| Path versioning | Versioned (`/iam/v1/`, `/registry/v1/`, `/vulnerabilities/v1/`, `/libraries/v1/`, `/advisory/v1/`) | Same domains with the `/v2/` segment | | Field naming | Inconsistent (`id`, `created_at`, `group_id`) | Consistent camelCase (`uid`, `createTime`/`updateTime`) | -| CREATE | Parent resource identified in the URL path | Parent identified in the request body | +| CREATE | Parent resource identified in the URL path | No change — parent stays in the URL path | | GET (single resource) | No dedicated endpoint; filter a List call | Dedicated Get endpoint, fetch by `uid` | | LIST | Offset/page-number params, full result set, no total count | `page_size`, `page_token`, `skip`, `order_by`, `total_count` | | UPDATE | PUT, full-resource replacement | PATCH, partial update via a field mask | | DELETE | Same path pattern, ambiguous failure on blocked deletes | Same path pattern, returns a `PreconditionFailure` detail when blocked | | Errors | Ambiguous — a missing resource can return an empty HTTP 200 result | Structured: `ErrorInfo`, `BadRequest`, `ResourceInfo`, `PreconditionFailure`, with proper status codes | -| Rate limiting | Not enforced | Enforced | ### Path prefixes by domain | Domain | v1 path prefix | v2 path prefix (GA) | | --- | --- | --- | -| IAM (Customer Platform: groups, roles, identities, role bindings) | `/iam/` | `/iam/v2/` | -| Registry (Containers) | `/registry/` | `/registry/v2/` | -| Vulnerabilities | `/vulnerabilities/` | `/vulnerabilities/v2/` | -| Libraries (Ecosystems) | `/libraries/` | `/libraries/v2/` | -| Advisory (Integrations) | `/advisory/` | `/advisory/v2/` | +| IAM (Customer Platform: groups, roles, identities, role bindings) | `/iam/v1/` | `/iam/v2/` | +| Registry (Containers) | `/registry/v1/` | `/registry/v2/` | +| Vulnerabilities | `/vulnerabilities/v1/` | `/vulnerabilities/v2/` | +| Libraries (Ecosystems) | `/libraries/v1/` | `/libraries/v2/` | +| Advisory (Integrations) | `/advisory/v1/` | `/advisory/v2/` | If you built against the beta endpoints, updating the version segment from `v2beta1` to `v2` is the only path change — the request and response shapes don't change as part of that rename. @@ -77,7 +75,7 @@ If you built against the beta endpoints, updating the version segment from `v2be ### Step 1: Update your endpoint paths -Add the `v2` version segment for each domain you call, using the [path prefix table](#path-prefixes-by-domain). If you were already on `/v2beta1/`, rename it to `/v2/`. +Change the version segment from `v1` to `v2` for each domain you call, using the [path prefix table](#path-prefixes-by-domain). If you were already on `/v2beta1/`, rename it to `/v2/`. ### Step 2: Update field references @@ -89,31 +87,7 @@ v2 standardizes field naming across every service: Update any code that reads specific field names out of API responses — these differ from v1 even for the same resource. -### Step 3: Move CREATE calls to parent-in-body - -v1 identifies the parent resource in the URL path (for example, `/groups/{parent}`). v2 moves the parent identifier into the request body instead, alongside the fields of the resource being created. - -**v1 (illustrative):** - -```shell -curl -X POST -H "Authorization: Bearer $TOKEN" \ - "$API/iam/groups/$PARENT_UID" \ - -d '{"name": "my-group"}' -``` - -**v2 (illustrative):** - -```shell -curl -X POST -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups" \ - -d '{"parent": "'"$PARENT_UID"'", "group": {"name": "my-group"}}' -``` - -> **CONFIRM WITH ENGINEERING:** this request body is illustrative. Get the exact body schema — including the `"group"` wrapper field name — from engineering before publishing. - -Stop building create-call URLs with the parent embedded in the path. Build a request body that includes both the parent identifier and the new resource's fields instead. - -### Step 4: Switch updates to PATCH with a field mask +### Step 3: Switch updates to PATCH with a field mask v1 uses PUT semantics: changing one field means sending the entire resource back, which forces a fetch-before-write pattern and creates race conditions on concurrent updates. v2 uses PATCH with a field mask, so you specify only the fields you're changing. @@ -123,13 +97,11 @@ curl -X PATCH -H "Authorization: Bearer $TOKEN" \ -d '{"description": "updated description"}' ``` -Testing against the live beta API confirmed this `updateMask` parameter name works. It's presumed unchanged at GA, but worth a final confirmation since none of the GA source docs restate it. - Replace fetch-modify-write update logic with a PATCH call that sets only the fields you're changing. A wildcard `*` is available if you genuinely want full replacement. Repository updates (`UpdateRepo`) are a common case to migrate here. For a worked repo PATCH example, see [Partial updates with FieldMask](/platform/api/api-v2-tutorial/#7-partial-updates-with-fieldmask) in the tutorial. -### Step 5: Switch pagination to cursor-based +### Step 4: Switch pagination to cursor-based v1 pagination relies on page numbers or offsets, and loads the full result set into memory. v2 List endpoints add: @@ -146,7 +118,7 @@ curl -H "Authorization: Bearer $TOKEN" \ Replace any page-number or offset logic with a loop that follows `page_token` until the response stops returning one. Use `skip` instead if you need to jump to an arbitrary page. Adopt `order_by` and drop any client-side sort you were doing to compensate. -### Step 6: Use dedicated Get endpoints +### Step 5: Use dedicated Get endpoints v1 typically requires filtering a List call to fetch a single resource. v2 adds a dedicated Get endpoint per resource, fetched by `uid` directly. @@ -157,7 +129,7 @@ curl -H "Authorization: Bearer $TOKEN" \ Replace any "list and filter to one item" pattern with the dedicated Get endpoint for that resource. -### Step 7: Update your error handling +### Step 6: Update your error handling In v1, an empty or ambiguous result can mean several different things — no matches, no permission, or a missing parent resource — with no way for a client to tell them apart. For example, requesting a resource that doesn't exist in v1 returns an empty result with HTTP 200. @@ -170,19 +142,9 @@ v2 returns proper status codes (for example, `NOT_FOUND` / HTTP 404) with struct If your integration currently treats an empty v1 result as ambiguous, or works around that ambiguity with extra calls, update it to branch on the v2 status code and structured error details instead. Re-test your error-handling paths, not just the happy path. -### Step 8: Review rate limits - -Rate limits were not enforced during beta. They are enforced starting at GA. - -> **CONFIRM WITH ENGINEERING:** specific rate limits and response headers (for example, remaining-quota headers). Add them here once confirmed, and link out to a rate-limits reference page if one exists. - -Review your call patterns — polling frequency, batch sizes — against the confirmed limits before cutting production traffic over. +### Step 7: Update gRPC and SDK clients -### Step 9: Update gRPC and SDK clients - -All v2 endpoints are also available directly via gRPC, at the same host. Proto definitions are at `chainguard.dev/sdk/proto/chainguard/platform/`. If you call the API via generated gRPC clients, regenerate them against the GA `v2` proto packages, renamed from `v2beta1`. - -> **CONFIRM WITH ENGINEERING:** whether the Go SDK client library actually moves from `chainguard.dev/sdk/proto/platform/clients/v2beta1` to an equivalent `v2` path at GA, or keeps the `v2beta1` package name internally even after the REST paths rename. +All v2 endpoints are also available directly via gRPC, at the same host. Proto definitions are at `chainguard.dev/sdk/proto/chainguard/platform/`. At GA the `v2beta1` designation is dropped across REST paths, gRPC proto packages, and the Go SDK. If you call the API via generated gRPC clients, regenerate them against the `v2` packages; the Go SDK clients aggregation moves to `chainguard.dev/sdk/proto/chainguard/platform/clients/v2`. ## Full example: listing and deleting groups @@ -190,7 +152,7 @@ All v2 endpoints are also available directly via gRPC, at the same host. Proto d ```shell curl -H "Authorization: Bearer $TOKEN" \ - "$API/iam/groups?uidp.descendants_of=$ORG_ID" + "$API/iam/v1/groups?uidp.descendants_of=$ORG_ID" ``` **After (v2):** @@ -215,16 +177,9 @@ Each `DELETE` returns an empty response body on success, same as v1. A delete bl ## Timeline -| Phase | When | What it means for you | -| --- | --- | --- | -| Parallel availability | GA onward | Both v1 and v2 serve traffic. v1 responses include deprecation headers noting the eventual sunset date. Migrate at your own pace. | -| Warning escalation | `CONFIRM WITH ENGINEERING` | v1 responses add an additional warning header. If you have meaningful v1 traffic, your Chainguard account team may reach out directly. | -| Soft shutdown | `CONFIRM WITH ENGINEERING` | v1 requests return an error with migration guidance, for a short grace period. | -| Hard removal | `CONFIRM WITH ENGINEERING` | v1 is fully removed. Requests to v1 endpoints fail. | +v1 remains fully supported alongside v2. This release sets no sunset date and adds no deprecation or sunset response headers. When a deprecation timeline is set, Chainguard will announce it separately, with generous advance notice. -> **CONFIRM WITH ENGINEERING:** every date in this timeline is a placeholder. Don't ship this table as-is. - -Don't wait for a hard deadline to start testing v2 — both versions are available today, and migration can happen incrementally. +You don't need to wait for a deadline to start: both versions are available today, and you can migrate incrementally. ## Troubleshooting @@ -237,11 +192,8 @@ No. Authentication and authorization are unchanged — the same tokens and ident **Why did an API call that used to return an empty list now return an error?** This is expected, and it's an improvement. In v1, a missing resource and an empty result look the same, so there was no way to tell "nothing matched" apart from "that resource doesn't exist" or "you don't have permission." v2 returns a specific, structured error in those cases instead. -**Why did my create call stop working?** -Check whether you're building the request URL with the parent resource in the path — that's the v1 pattern. In v2, the parent goes in the request body instead. See [Step 3](#step-3-move-create-calls-to-parent-in-body). - **Where do I send a customer who asks about migrating?** -This guide. If you're on the account or support team, reach out to Engineering for anything marked `CONFIRM WITH ENGINEERING` in this guide rather than guessing at specifics. +This guide. If you're on the account or support team and hit something this guide doesn't cover, reach out to Engineering rather than guessing at specifics. ## Next steps diff --git a/content/platform/api/api-v2-tutorial.md b/content/platform/api/api-v2-tutorial.md index dbc58fb5ce..643821c27f 100644 --- a/content/platform/api/api-v2-tutorial.md +++ b/content/platform/api/api-v2-tutorial.md @@ -6,7 +6,7 @@ linktitle: "API v2 Tutorial" type: "article" description: "Tutorial with examples showing how you can use the Chainguard API v2." date: 2026-03-30T08:49:31+00:00 -lastmod: 2026-07-20T00:00:00+00:00 +lastmod: 2026-08-04T13:01:07+00:00 draft: false tags: ["Chainguard Console", "Procedural"] images: [] @@ -14,8 +14,6 @@ toc: true weight: 030 --- -> **Draft status:** This page has several items marked `CONFIRM WITH ENGINEERING`. Do not publish externally until those are resolved. - The v2 API is now Generally Available (GA) and introduces cursor-based pagination, server-side ordering, consistent resource patterns, and structured error responses across all endpoints. This guide walks through the v2 API using real `curl` commands. If you're migrating an existing v1 or beta (`v2beta1`) integration, see [Migrating from API v1 to API v2](/platform/api/api-v2-migration/) instead. @@ -42,15 +40,16 @@ This guide walks through the v2 API using real `curl` commands. If you're migrat | Domain | Resources | Operations | | -------- | ----------- | ------------ | -| **IAM** | Groups, Identities, Roles, RoleBindings, IdentityProviders, AccountAssociations, GroupInvites | List, Get, Create, Update, Delete | -| **Registry** | Repos, Tags | List, Get | +| **IAM** | Groups, Identities, Roles, RoleBindings, IdentityProviders, AccountAssociations, GroupInvites, Terms, ExternalGroupRoleMappings | List, Get, Create, Update, Delete | +| **Registry** | Repos, Tags, Images | List, Get, Create, Update, Delete | | **Vulnerabilities** | Advisories | List, Get | -| **Ecosystems (Libraries)** | — | — | -| **Integrations (Advisory)** | — | — | +| **Ecosystems (Libraries)** | Artifacts | List, Get (read-only) | +| **Integrations (Advisory)** | SecurityAdvisory (documents, metadata, resolved-vuln reports) | List (read-only) | +| **Events** | Subscriptions | List, Get, Create, Delete | -All endpoints live under a versioned path per domain: `/iam/v2/`, `/registry/v2/`, `/vulnerabilities/v2/`, `/libraries/v2/`, or `/advisory/v2/`. +All endpoints live under a versioned path per domain: `/iam/v2/`, `/registry/v2/`, `/vulnerabilities/v2/`, `/libraries/v2/`, `/advisory/v2/`, or `/events/v2/`. -> **CONFIRM WITH ENGINEERING:** Ecosystems and Integrations are new domains at GA (beta only covered IAM, Registry, and Vulnerabilities). Get the specific resource names and supported operations for these two domains before publishing — the worked examples in this guide only cover the three domains available during beta. +The worked examples in this guide focus on IAM, Registry, and Vulnerabilities. The other domains follow the same request and response conventions. ## Prerequisites @@ -70,10 +69,7 @@ The following examples use `$TOKEN`, `$API`, and `$ORG_ID` for brevity. Keep the following in mind as you work through this guide. - **Page tokens expire after 3 days** ([AIP-158](https://google.aip.dev/158)). If a token expires, the query restarts from the beginning — no error is returned. -- **Rate limits** are enforced starting at GA. -- **gRPC** — all endpoints are also available via gRPC at the same host. Proto definitions are at `chainguard.dev/sdk/proto/chainguard/platform/`. - -> **CONFIRM WITH ENGINEERING:** specific rate limits and response headers, and whether the Go SDK client library moves from `chainguard.dev/sdk/proto/platform/clients/v2beta1` to a `v2` path at GA. Don't publish this section externally until both are confirmed. +- **gRPC** — all endpoints are also available via gRPC at the same host. Proto definitions are at `chainguard.dev/sdk/proto/chainguard/platform/`, and the Go SDK clients live under `chainguard.dev/sdk/proto/chainguard/platform/clients/v2`. --- @@ -182,8 +178,8 @@ A real workflow: create an org folder, add an identity, and bind a role. ```shell curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2/groups" \ - -d '{"parent": "'"$ORG_ID"'", "group": {"name": "backend-team", "description": "Backend engineering team"}}' | jq . + "$API/iam/v2/groups/$ORG_ID" \ + -d '{"name": "backend-team", "description": "Backend engineering team"}' | jq . ``` ```json @@ -198,9 +194,7 @@ curl -s -X POST -H "Authorization: Bearer $TOKEN" \ } ``` -> **Note:** The parent group goes in the request body, not the URL path — a change from beta. See [Migrating from API v1 to API v2](/platform/api/api-v2-migration/#step-3-move-create-calls-to-parent-in-body) for details. -> -> **CONFIRM WITH ENGINEERING:** this request body shape, including the `"group"` wrapper field name, is illustrative. Confirm the exact schema before publishing. +> **Note:** The parent group goes in the URL path (`/groups/{parent}`). The request body carries only the resource fields — the same pattern as v1. ### Create an identity @@ -210,22 +204,17 @@ export GROUP_UID=YOUR_GROUP_UID curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2/identities" \ + "$API/iam/v2/identities/$GROUP_UID" \ -d '{ - "parent": "'"$GROUP_UID"'", - "identity": { - "name": "ci-bot", - "description": "CI/CD pipeline identity", - "claimMatch": { - "issuer": "https://token.actions.githubusercontent.com", - "subject": "repo:my-org@123456/my-repo@654321:ref:refs/heads/main" - } + "name": "ci-bot", + "description": "CI/CD pipeline identity", + "claimMatch": { + "issuer": "https://token.actions.githubusercontent.com", + "subject": "repo:my-org@123456/my-repo@654321:ref:refs/heads/main" } }' | jq . ``` -> **CONFIRM WITH ENGINEERING:** this request body shape, including the `"identity"` wrapper field name, is illustrative. Confirm the exact schema before publishing. - ```json { "uid": "d9e2f1a0.../fb139588d99c8efe/f462d354ca32ca9f", @@ -274,12 +263,10 @@ export IDENTITY_UID=YOUR_IDENTITY_UID curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2/roleBindings" \ - -d "{\"parent\": \"$GROUP_UID\", \"roleBinding\": {\"identityUid\": \"$IDENTITY_UID\", \"roleUid\": \"$ROLE_UID\"}}" | jq . + "$API/iam/v2/roleBindings/$GROUP_UID" \ + -d "{\"identityUid\": \"$IDENTITY_UID\", \"roleUid\": \"$ROLE_UID\"}" | jq . ``` -> **CONFIRM WITH ENGINEERING:** this request body shape, including the `"roleBinding"` wrapper field name, is illustrative. Confirm the exact schema before publishing. - ```json { "uid": "d9e2f1a0.../fb139588.../9b822036a7075d75", @@ -484,7 +471,7 @@ curl -s -H "Authorization: Bearer $TOKEN" \ ### Check for deprecated tags -In v1, a dedicated `ListEolTags` call surfaced end-of-life tags. v2beta1 has no separate end-of-life endpoint or server-side filter. Instead, each tag carries a `deprecated` boolean, which you filter on client-side: +In v1, a dedicated `ListEolTags` call surfaced end-of-life tags. v2 has no separate end-of-life endpoint or server-side filter. Instead, each tag carries a `deprecated` boolean, which you filter on client-side: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ @@ -492,13 +479,13 @@ curl -s -H "Authorization: Bearer $TOKEN" \ | jq '[.tags[] | select(.deprecated) | .name]' ``` -> **CONFIRM WITH ENGINEERING:** v2beta1 exposes tag deprecation only as a per-tag `deprecated` field, with no server-side end-of-life filter. Confirm whether GA adds a dedicated filter or endpoint, since v1's `ListEolTags` was a common direct-API call. +> **Note:** Client-side filtering on `deprecated` is the intended approach in v2. A v2 equivalent of `ListEolTags` is on the backlog with no committed date; until it ships, the v1 `ListEolTags` endpoint remains available. --- ## 5. Querying vulnerabilities -The Vulnerabilities domain exposes advisory data. In v2beta1 it covers advisories with List and Get. +The Vulnerabilities domain exposes advisory data. In v2 it covers advisories with List and Get. ### List advisories @@ -522,7 +509,7 @@ Fetch a single advisory by UID at `/vulnerabilities/v2/advisories/{uid}`. ### Vulnerability reports -> **CONFIRM WITH ENGINEERING:** the v1 `GetVulnReport` and `ListVulnCountReports` calls — used heavily by direct HTTP integrations — have no equivalent in the v2beta1 spec, which exposes only advisories. Confirm whether GA adds vulnerability-report endpoints, or whether advisory data is meant to replace them. This is a real migration gap for the CI/CD audience that reads scan results programmatically. +The v1 `GetVulnReport` and `ListVulnCountReports` calls have no v2 equivalent today, and advisories are not a replacement — they serve a different, advisory-feed purpose. The `ListResolvedVulnsReports` endpoint under `/advisory/v2/` is also advisory-feed oriented, not a vulnerability-report replacement. If your integration reads vulnerability reports, continue using the v1 endpoints, which remain fully supported. v2 coverage arrives with the Vulnerabilities domain's migration. --- @@ -535,8 +522,8 @@ API v2 returns structured error responses with machine-parseable codes and detai ```shell curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2/groups" \ - -d '{"parent": "'"$ORG_ID"'", "group": {}}' | jq . + "$API/iam/v2/groups/$ORG_ID" \ + -d '{}' | jq . ``` ```json @@ -659,7 +646,7 @@ curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \ } ``` -> **Note:** The repo update path (`/registry/v2/repos/{repo.uid}`) and request body are confirmed against the published v2beta1 spec, where `name` and `description` are writable. As with the group example, `updateMask` is accepted by the live API but isn't restated in the spec, so reconfirm it holds at GA. +> **Note:** Repo updates follow the same PATCH-with-field-mask pattern as groups; `name` and `description` are writable. --- From 7f404dc014241463d2b6fc41c162c3dc7cac3873 Mon Sep 17 00:00:00 2001 From: Matthew Helmke Date: Tue, 4 Aug 2026 09:31:22 -0500 Subject: [PATCH 5/6] edits from docs review --- content/platform/api/api-v2-migration.md | 36 ++++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/content/platform/api/api-v2-migration.md b/content/platform/api/api-v2-migration.md index e37af18576..ce60f213dc 100644 --- a/content/platform/api/api-v2-migration.md +++ b/content/platform/api/api-v2-migration.md @@ -6,7 +6,7 @@ linktitle: "API v1 to v2 Migration" type: "article" description: "How to migrate a direct integration with the Chainguard API from v1 to the GA API v2." date: 2026-07-20T00:00:00+00:00 -lastmod: 2026-08-04T13:01:07+00:00 +lastmod: 2026-08-04T14:31:22+00:00 draft: false tags: ["Chainguard Console", "Procedural"] images: [] @@ -25,7 +25,7 @@ v1 continues to work during a transition period, so nothing breaks today. Migrat You'll need: - A valid Chainguard identity with access to the resources you're calling. -- `chainctl` installed, to mint a token for testing (`chainctl auth token`). +- [`chainctl` installed](/platform/chainctl-usage/how-to-install-chainctl/), to mint a token for testing (`chainctl auth token`). Set up your environment for the following examples: @@ -38,11 +38,11 @@ export ORG_ID=YOUR_ORG_ID ## What's not changing -- **Authentication** — same OIDC token model as v1. -- **Authorization** — same identity-based access control. -- **Scoping** — same `uidp.descendants_of` / `uidp.children_of` hierarchy filters. -- **Host** — same API host (`console-api.enforce.dev` in production). -- **CREATE shape** — the parent resource stays in the URL path (`/groups/{parent}`), and the request body carries the resource fields directly. This is unchanged from v1. +- **Authentication**: same OIDC token model as v1. +- **Authorization**: same identity-based access control. +- **Scoping**: same `uidp.descendants_of` / `uidp.children_of` hierarchy filters. +- **Host**: same API host (`console-api.enforce.dev` in production). +- **CREATE shape**: the parent resource stays in the URL path (`/groups/{parent}`), and the request body carries the resource fields directly. This is unchanged from v1. If your integration already authenticates against v1 successfully, no credential or auth-flow changes are needed to call v2. @@ -105,11 +105,11 @@ Repository updates (`UpdateRepo`) are a common case to migrate here. For a worke v1 pagination relies on page numbers or offsets, and loads the full result set into memory. v2 List endpoints add: -- `page_size` — how many items to return per page. -- `page_token` — the opaque cursor from the previous response's `next_page_token`, used to fetch the next page. Omit it for the first page. -- `skip` — number of items to skip, for jumping directly to a specific page without walking the cursor sequentially. -- `order_by` — server-side ordering, instead of sorting client-side after fetching. -- `total_count` — total matching items, so you don't have to infer it from page math. +- `page_size`: how many items to return per page. +- `page_token`: the opaque cursor from the previous response's `next_page_token`, used to fetch the next page. Omit it for the first page. +- `skip`: number of items to skip, for jumping directly to a specific page without walking the cursor sequentially. +- `order_by`: server-side ordering, instead of sorting client-side after fetching. +- `total_count`: total matching items, so you don't have to infer it from page math. ```shell curl -H "Authorization: Bearer $TOKEN" \ @@ -135,10 +135,10 @@ In v1, an empty or ambiguous result can mean several different things — no mat v2 returns proper status codes (for example, `NOT_FOUND` / HTTP 404) with structured error details, following the same error model used across Google Cloud APIs: -- **ErrorInfo** — a machine-readable reason code and error domain. -- **BadRequest** — per-field validation errors. -- **ResourceInfo** — which specific resource type and name wasn't found. -- **PreconditionFailure** — why a request was blocked (for example, a delete blocked by a dependent resource). +- **ErrorInfo**: a machine-readable reason code and error domain. +- **BadRequest**: per-field validation errors. +- **ResourceInfo**: which specific resource type and name wasn't found. +- **PreconditionFailure**: why a request was blocked (for example, a delete blocked by a dependent resource). If your integration currently treats an empty v1 result as ambiguous, or works around that ambiguity with extra calls, update it to branch on the v2 status code and structured error details instead. Re-test your error-handling paths, not just the happy path. @@ -197,6 +197,6 @@ This guide. If you're on the account or support team and hit something this guid ## Next steps -- [Chainguard API v2 Tutorial](/platform/api/api-v2-tutorial/) — worked examples of the v2 endpoints you'll be migrating to. -- [Chainguard API v2 Specification](/platform/api/spec-api-v2/) — full OpenAPI reference. +- [Chainguard API v2 Tutorial](/platform/api/api-v2-tutorial/): worked examples of the v2 endpoints you'll be migrating to. +- [Chainguard API v2 Specification](/platform/api/spec-api-v2/): full OpenAPI reference. - Reach out to your Chainguard account team or [Chainguard Support](https://support.chainguard.dev/) if you run into issues migrating. From 3a5963012a1f387ae422ff609cf1029579e9c825 Mon Sep 17 00:00:00 2001 From: Matthew Helmke Date: Fri, 7 Aug 2026 08:02:36 -0500 Subject: [PATCH 6/6] docs: re-orient API v2 tutorial to repos; fix subgroup access guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Customer feedback (via engineering) showed the "set up access" walkthrough was actively misleading: it created a subgroup, then created a CI identity inside that subgroup and bound the role there — leaving the identity unable to see the registry, which lives under the root org group. - §2: stop creating a subgroup. Create the identity under the root org group and bind the role there, so it can reach the registry. Add a note explaining why root-level placement matters. - §1 and §3: lead with repos instead of groups — the resource customers actually have — and drop the nested-subgroup filter example. - §4: dedupe to tags and end-of-life (repo list/get now lead in §1). - §6: trigger the validation-error example with an identity create rather than a subgroup create. - §7: unify the field-mask examples on repos. - Cleanup: drop the group deletion. - Migration guide: bring examples to parity — Steps 3, 4, 5 and the full example now use repos. Co-Authored-By: Claude Opus 4.8 --- content/platform/api/api-v2-migration.md | 24 +-- content/platform/api/api-v2-tutorial.md | 240 +++++++---------------- 2 files changed, 84 insertions(+), 180 deletions(-) diff --git a/content/platform/api/api-v2-migration.md b/content/platform/api/api-v2-migration.md index ce60f213dc..33c6118e69 100644 --- a/content/platform/api/api-v2-migration.md +++ b/content/platform/api/api-v2-migration.md @@ -6,7 +6,7 @@ linktitle: "API v1 to v2 Migration" type: "article" description: "How to migrate a direct integration with the Chainguard API from v1 to the GA API v2." date: 2026-07-20T00:00:00+00:00 -lastmod: 2026-08-04T14:31:22+00:00 +lastmod: 2026-08-07T13:02:36+00:00 draft: false tags: ["Chainguard Console", "Procedural"] images: [] @@ -93,7 +93,7 @@ v1 uses PUT semantics: changing one field means sending the entire resource back ```shell curl -X PATCH -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups/$GROUP_UID?updateMask=description" \ + "$API/registry/v2/repos/$REPO_UID?updateMask=description" \ -d '{"description": "updated description"}' ``` @@ -113,7 +113,7 @@ v1 pagination relies on page numbers or offsets, and loads the full result set i ```shell curl -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=3&order_by=name" | jq . + "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&page_size=3&order_by=name" | jq . ``` Replace any page-number or offset logic with a loop that follows `page_token` until the response stops returning one. Use `skip` instead if you need to jump to an arbitrary page. Adopt `order_by` and drop any client-side sort you were doing to compensate. @@ -124,7 +124,7 @@ v1 typically requires filtering a List call to fetch a single resource. v2 adds ```shell curl -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups/$GROUP_UID" + "$API/registry/v2/repos/$REPO_UID" ``` Replace any "list and filter to one item" pattern with the dedicated Get endpoint for that resource. @@ -146,34 +146,30 @@ If your integration currently treats an empty v1 result as ambiguous, or works a All v2 endpoints are also available directly via gRPC, at the same host. Proto definitions are at `chainguard.dev/sdk/proto/chainguard/platform/`. At GA the `v2beta1` designation is dropped across REST paths, gRPC proto packages, and the Go SDK. If you call the API via generated gRPC clients, regenerate them against the `v2` packages; the Go SDK clients aggregation moves to `chainguard.dev/sdk/proto/chainguard/platform/clients/v2`. -## Full example: listing and deleting groups +## Full example: listing and deleting repos **Before (v1):** ```shell curl -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v1/groups?uidp.descendants_of=$ORG_ID" + "$API/registry/v1/repos?uidp.descendants_of=$ORG_ID" ``` **After (v2):** ```shell curl -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=3&order_by=name" | jq . + "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&page_size=3&order_by=name" | jq . ``` -Delete calls follow the same path-prefix change, with no other behavior change: +Delete follows the same path-prefix change, with no other behavior change: ```shell curl -X DELETE -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/roleBindings/$BINDING_UID" -curl -X DELETE -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/identities/$IDENTITY_UID" -curl -X DELETE -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups/$GROUP_UID" + "$API/registry/v2/repos/$REPO_UID" ``` -Each `DELETE` returns an empty response body on success, same as v1. A delete blocked by a dependent resource now returns a `PreconditionFailure` error detail explaining why, instead of an ambiguous failure. +A `DELETE` returns an empty response body on success, same as v1. A delete blocked by a dependent resource now returns a `PreconditionFailure` error detail explaining why, instead of an ambiguous failure. ## Timeline diff --git a/content/platform/api/api-v2-tutorial.md b/content/platform/api/api-v2-tutorial.md index 643821c27f..e3448f2de0 100644 --- a/content/platform/api/api-v2-tutorial.md +++ b/content/platform/api/api-v2-tutorial.md @@ -6,7 +6,7 @@ linktitle: "API v2 Tutorial" type: "article" description: "Tutorial with examples showing how you can use the Chainguard API v2." date: 2026-03-30T08:49:31+00:00 -lastmod: 2026-08-04T13:01:07+00:00 +lastmod: 2026-08-07T13:02:36+00:00 draft: false tags: ["Chainguard Console", "Procedural"] images: [] @@ -75,46 +75,37 @@ Keep the following in mind as you work through this guide. ## 1. Your first v2 request -List the first 3 groups in your organization: +List the first 3 repos in your organization: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=3&order_by=name" | jq . + "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&page_size=3&order_by=name" | jq . ``` ```json { - "groups": [ + "repos": [ { - "uid": "d9e2f1a0.../9f1c889071ceb6bf", - "name": "api", - "description": "API services and backend", - "resourceLimits": {}, - "verified": false, - "createTime": "2026-03-27T13:20:03.456Z", - "updateTime": "2026-03-27T13:20:03.456Z" + "uid": "d9e2f1a0.../06626efd8c6b3fb7", + "name": "nginx", + "createTime": "2026-01-28T12:54:21.189Z", + "updateTime": "2026-01-28T12:54:21.189Z" }, { - "uid": "d9e2f1a0.../822b6e789e77ebb9", - "name": "base-images", - "description": "Base image maintenance", - "resourceLimits": {}, - "verified": false, - "createTime": "2026-03-27T13:20:03.123Z", - "updateTime": "2026-03-27T13:20:03.123Z" + "uid": "d9e2f1a0.../0ed18f0f929f4c60", + "name": "python", + "createTime": "2026-01-23T14:54:42.774Z", + "updateTime": "2026-01-23T14:54:42.774Z" }, { - "uid": "d9e2f1a0.../251da0851a321620", - "name": "ci-cd", - "description": "CI/CD pipelines and automation", - "resourceLimits": {}, - "verified": false, - "createTime": "2026-03-27T13:20:03.789Z", - "updateTime": "2026-03-27T13:20:03.789Z" + "uid": "d9e2f1a0.../12b4208b23740c37", + "name": "static", + "createTime": "2026-01-23T14:54:39.021Z", + "updateTime": "2026-01-23T14:54:39.021Z" } ], "nextPageToken": "CqQBV3lK...", - "totalCount": "14", + "totalCount": "12", "skipped": 0 } ``` @@ -131,15 +122,18 @@ Every v2 response follows the same shape: New in v2: fetch a resource directly by UID. In v1, this required a List call with an ID filter. ```shell +# REPO_UID is a uid value from the List repos response above +export REPO_UID=YOUR_REPO_UID + curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups/$GROUP_UID" | jq '{uid, name, description}' + "$API/registry/v2/repos/$REPO_UID" | jq '{uid, name, createTime}' ``` ```json { - "uid": "d9e2f1a0.../04b8bc5bcb561945", - "name": "engineering", - "description": "Engineering department" + "uid": "d9e2f1a0.../06626efd8c6b3fb7", + "name": "nginx", + "createTime": "2026-01-28T12:54:21.189Z" } ``` @@ -147,20 +141,20 @@ Use direct UID lookups when you already know the resource identifier — they ar ### Filter by name -Find a specific group without knowing its UID: +Find a specific repo without knowing its UID: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&name=platform" \ - | jq '[.groups[] | {uid, name, description}]' + "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&name=nginx" \ + | jq '[.repos[] | {uid, name, createTime}]' ``` ```json [ { - "uid": "d9e2f1a0.../04b8bc5bcb561945/3af5754ef8e5dd4d", - "name": "platform", - "description": "Platform team — infrastructure and developer tools" + "uid": "d9e2f1a0.../06626efd8c6b3fb7", + "name": "nginx", + "createTime": "2026-01-28T12:54:21.189Z" } ] ``` @@ -171,40 +165,16 @@ Name filtering returns exact matches. Combine with `uidp.descendants_of` to scop ## 2. Set up access for a new team -A real workflow: create an org folder, add an identity, and bind a role. - -### Create a group +A common workflow: create a CI identity at your organization and bind a role to it. -```shell -curl -s -X POST -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - "$API/iam/v2/groups/$ORG_ID" \ - -d '{"name": "backend-team", "description": "Backend engineering team"}' | jq . -``` - -```json -{ - "uid": "d9e2f1a0.../fb139588d99c8efe", - "name": "backend-team", - "description": "Backend engineering team", - "resourceLimits": {}, - "verified": false, - "createTime": "2026-03-27T13:55:00.423Z", - "updateTime": "2026-03-27T13:55:00.423Z" -} -``` - -> **Note:** The parent group goes in the URL path (`/groups/{parent}`). The request body carries only the resource fields — the same pattern as v1. +> **Note:** Create identities under your root organization group (`$ORG_ID`) so they can reach the resources that live there, including your registry. An identity created under a subgroup is scoped to that subgroup and won't see your registry — so it can't pull images. ### Create an identity ```shell -# GROUP_UID is the uid value returned in the Create a group response above -export GROUP_UID=YOUR_GROUP_UID - curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2/identities/$GROUP_UID" \ + "$API/iam/v2/identities/$ORG_ID" \ -d '{ "name": "ci-bot", "description": "CI/CD pipeline identity", @@ -217,7 +187,7 @@ curl -s -X POST -H "Authorization: Bearer $TOKEN" \ ```json { - "uid": "d9e2f1a0.../fb139588d99c8efe/f462d354ca32ca9f", + "uid": "d9e2f1a0.../f462d354ca32ca9f", "name": "ci-bot", "description": "CI/CD pipeline identity", "lastSeenTime": "2026-03-27T13:55:00.783Z", @@ -263,24 +233,24 @@ export IDENTITY_UID=YOUR_IDENTITY_UID curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2/roleBindings/$GROUP_UID" \ + "$API/iam/v2/roleBindings/$ORG_ID" \ -d "{\"identityUid\": \"$IDENTITY_UID\", \"roleUid\": \"$ROLE_UID\"}" | jq . ``` ```json { - "uid": "d9e2f1a0.../fb139588.../9b822036a7075d75", + "uid": "d9e2f1a0.../9b822036a7075d75", "identity": { - "uid": "d9e2f1a0.../fb139588.../f462d354ca32ca9f", + "uid": "d9e2f1a0.../f462d354ca32ca9f", "name": "ci-bot", "description": "CI/CD pipeline identity", "subject": "repo:my-org@123456/my-repo@654321:ref:refs/heads/main", "issuer": "https://token.actions.githubusercontent.com" }, "group": { - "uid": "d9e2f1a0.../fb139588d99c8efe", - "name": "backend-team", - "description": "Backend engineering team" + "uid": "d9e2f1a0...", + "name": "my-org", + "description": "Root organization group" }, "role": { "uid": "63921b2c44617e3f2603851537be0123af4a57d7", @@ -303,14 +273,14 @@ Every List endpoint supports cursor-based pagination with consistent parameters. ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5" \ - | jq '{totalCount, groups: [.groups[].name], nextPageToken: .nextPageToken[:20]}' + "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&page_size=5" \ + | jq '{totalCount, repos: [.repos[].name], nextPageToken: .nextPageToken[:20]}' ``` ```json { - "totalCount": "14", - "groups": ["api", "base-images", "ci-cd", "containers", "engineering"], + "totalCount": "12", + "repos": ["apko", "busybox", "go", "jdk", "nginx"], "nextPageToken": "CqQBV3lKbE16Z3dPVE0y" } ``` @@ -319,13 +289,13 @@ Follow the cursor for the next page: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5&page_token=CqQBV3lK..." \ - | jq '{groups: [.groups[].name]}' + "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&page_size=5&page_token=CqQBV3lK..." \ + | jq '{repos: [.repos[].name]}' ``` ```json { - "groups": ["incident-response", "platform", "production", "registry-ops", "root"] + "repos": ["node", "php", "postgres", "python", "redis"] } ``` @@ -337,41 +307,41 @@ Sort by name: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name" \ - | jq '[.groups[].name]' + "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name" \ + | jq '[.repos[].name]' ``` ```json -["api", "base-images", "ci-cd", "containers", "engineering"] +["apko", "busybox", "go", "jdk", "nginx"] ``` Reverse the order: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name%20desc" \ - | jq '[.groups[].name]' + "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name%20desc" \ + | jq '[.repos[].name]' ``` ```json -["vuln-scanning", "staging", "security", "sandbox", "root"] +["static", "ruby", "redis", "python", "postgres"] ``` Sort by creation time (newest first): ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=created_at%20desc" \ - | jq '[.groups[] | {name, createTime}]' + "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&page_size=5&order_by=created_at%20desc" \ + | jq '[.repos[] | {name, createTime}]' ``` ```json [ - {"name": "sandbox", "createTime": "2026-03-27T13:20:05.488Z"}, - {"name": "production", "createTime": "2026-03-27T13:20:05.135Z"}, - {"name": "staging", "createTime": "2026-03-27T13:20:04.814Z"}, - {"name": "incident-response", "createTime": "2026-03-27T13:20:04.257Z"}, - {"name": "vuln-scanning", "createTime": "2026-03-27T13:20:03.915Z"} + {"name": "redis", "createTime": "2026-02-14T09:11:05.488Z"}, + {"name": "postgres", "createTime": "2026-02-10T17:02:05.135Z"}, + {"name": "node", "createTime": "2026-02-03T11:48:04.814Z"}, + {"name": "nginx", "createTime": "2026-01-28T12:54:21.189Z"}, + {"name": "python", "createTime": "2026-01-23T14:54:42.774Z"} ] ``` @@ -383,14 +353,14 @@ Jump directly to page 3 (skip the first 10 results): ```shell curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/iam/v2/groups?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name&skip=10" \ - | jq '{skipped: .skipped, groups: [.groups[].name]}' + "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&page_size=5&order_by=name&skip=10" \ + | jq '{skipped: .skipped, repos: [.repos[].name]}' ``` ```json { "skipped": 10, - "groups": ["sandbox", "security", "staging", "vuln-scanning"] + "repos": ["ruby", "static"] } ``` @@ -407,53 +377,13 @@ The `skipped` field in the response confirms how many results were skipped, usef --- -## 4. Querying the registry - -Registry endpoints follow the same List, Get, and pagination patterns as the IAM examples earlier, applied to repos and tags. If your integration reads image metadata — listing repos, walking tags, or checking end-of-life status — these are the endpoints you call most. - -### List repos - -List repos scoped to your organization: - -```shell -curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/registry/v2/repos?uidp.descendants_of=$ORG_ID&page_size=3" \ - | jq '[.repos[] | {uid, name, createTime}]' -``` - -```json -[ - {"uid": "d9e2f1a0.../06626efd8c6b3fb7", "name": "nginx", "createTime": "2026-01-28T12:54:21.189Z"}, - {"uid": "d9e2f1a0.../0ed18f0f929f4c60", "name": "python", "createTime": "2026-01-23T14:54:42.774Z"}, - {"uid": "d9e2f1a0.../12b4208b23740c37", "name": "static", "createTime": "2026-01-23T14:54:39.021Z"} -] -``` - -The same pagination and ordering parameters work on every List endpoint. - -### Get a single repo +## 4. Tags and end-of-life -Fetch one repo directly by UID, the same way you fetch a group: - -```shell -# REPO_UID is a uid value from the List repos response above -export REPO_UID=YOUR_REPO_UID - -curl -s -H "Authorization: Bearer $TOKEN" \ - "$API/registry/v2/repos/$REPO_UID" | jq '{uid, name, createTime}' -``` - -```json -{ - "uid": "d9e2f1a0.../06626efd8c6b3fb7", - "name": "nginx", - "createTime": "2026-01-28T12:54:21.189Z" -} -``` +Tags live under a repo. List them with the same patterns you used for repos, scoped to a single repo with `uidp.children_of`. ### List tags in a repo -Scope a tag listing to a single repo with `uidp.children_of`. Each tag carries its `digest` and a `deprecated` flag: +Each tag carries its `digest` and a `deprecated` flag: ```shell curl -s -H "Authorization: Bearer $TOKEN" \ @@ -522,7 +452,7 @@ API v2 returns structured error responses with machine-parseable codes and detai ```shell curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2/groups/$ORG_ID" \ + "$API/iam/v2/identities/$ORG_ID" \ -d '{}' | jq . ``` @@ -589,7 +519,7 @@ Update specific fields without sending the full resource. Only the fields listed ```shell curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2/groups/$GROUP_UID" \ + "$API/registry/v2/repos/$REPO_UID" \ -d '{ "description": "Updated description — only this field changes" }' | jq '{uid, name, description}' @@ -597,8 +527,8 @@ curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \ ```json { - "uid": "d9e2f1a0.../fb139588d99c8efe", - "name": "backend-team", + "uid": "d9e2f1a0.../06626efd8c6b3fb7", + "name": "nginx", "description": "Updated description — only this field changes" } ``` @@ -610,43 +540,22 @@ To be explicit about which fields to update, pass `updateMask`: ```shell curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - "$API/iam/v2/groups/$GROUP_UID?updateMask=description" \ + "$API/registry/v2/repos/$REPO_UID?updateMask=description" \ -d '{ "description": "Only this field is updated", "name": "this-is-ignored" }' | jq '{uid, name, description}' ``` -```json -{ - "uid": "d9e2f1a0.../fb139588d99c8efe", - "name": "backend-team", - "description": "Only this field is updated" -} -``` - -The `name` in the body is ignored because `updateMask` only includes `description`. - -### Update a repo - -The same PATCH-with-field-mask pattern applies to every updatable resource. Repository updates are a common case to migrate from v1, where changing one field meant sending the full resource: - -```shell -curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - "$API/registry/v2/repos/$REPO_UID?updateMask=description" \ - -d '{"description": "Updated repo description"}' | jq '{uid, name, description}' -``` - ```json { "uid": "d9e2f1a0.../06626efd8c6b3fb7", "name": "nginx", - "description": "Updated repo description" + "description": "Only this field is updated" } ``` -> **Note:** Repo updates follow the same PATCH-with-field-mask pattern as groups; `name` and `description` are writable. +The `name` in the body is ignored because `updateMask` only includes `description`. This PATCH-with-field-mask pattern applies to every updatable resource. --- @@ -661,11 +570,10 @@ v2 is additive — v1 endpoints remain available during a transition period, so Delete resources you created during this walkthrough: ```shell -# Delete in reverse order: role binding, identity, group +# Delete in reverse order: role binding, then identity # BINDING_UID is the uid value returned in the Bind a role response above curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$API/iam/v2/roleBindings/$BINDING_UID" curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$API/iam/v2/identities/$IDENTITY_UID" -curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$API/iam/v2/groups/$GROUP_UID" ``` Each DELETE returns an empty response body on success.