diff --git a/Dockerfile b/Dockerfile
index b0ab6a5993cf..c0d288a83d11 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,6 +12,7 @@ ARG PAGEFIND_VERSION=1.5.2
# base defines the generic base stage
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
RUN apk add --no-cache \
+ bash \
git \
nodejs \
npm \
@@ -41,6 +42,9 @@ WORKDIR /project
COPY --from=hugo /out/hugo /bin/hugo
COPY --from=npm /out/node_modules node_modules
COPY . .
+RUN --mount=type=cache,target=/root/go/pkg/mod \
+ --mount=type=cache,target=/root/.cache/go-build \
+ ./hack/api-docs/run.sh test && ./hack/api-docs/run.sh generate
# build creates production builds with Hugo
FROM build-base AS build
@@ -58,7 +62,9 @@ RUN --mount=type=cache,target=/tmp/hugo_cache \
--printUnusedTemplates \
-b $DOCS_URL \
-e $HUGO_ENV
-RUN ./hack/flatten-and-resolve.js public
+RUN node --test hack/test/flatten-and-resolve.mjs
+RUN node hack/flatten-and-resolve.js public
+RUN node hack/api-docs/verify-output.mjs public
# lint lints markdown files
FROM ghcr.io/rvben/rumdl:0.2.49-alpine AS lint
diff --git a/_vale/config/vocabularies/Docker/accept.txt b/_vale/config/vocabularies/Docker/accept.txt
index 75ac4e3e93b7..910369f45322 100644
--- a/_vale/config/vocabularies/Docker/accept.txt
+++ b/_vale/config/vocabularies/Docker/accept.txt
@@ -449,3 +449,4 @@ superset
tokenization
WebGL
Xubuntu
+Netlify
diff --git a/assets/api-reference/reference.css b/assets/api-reference/reference.css
new file mode 100644
index 000000000000..862feee37c89
--- /dev/null
+++ b/assets/api-reference/reference.css
@@ -0,0 +1,596 @@
+/* Scoped reference tokens inherit Docker's palette and installed typefaces. */
+.api-reference,
+.api-nav {
+ --api-text: var(--color-gray-800, #2c333f);
+ --api-muted: var(--color-gray-600, #566581);
+ --api-line: var(--color-gray-100, #e7eaef);
+ --api-surface: var(--color-background-light, #f9f9fa);
+ --api-paper: #fff;
+ --api-link: var(--color-blue-600, #0d4df2);
+ --api-selected: var(--color-blue-50, #f6f8fe);
+ --api-mono: "Roboto Mono", ui-monospace, monospace;
+ color: var(--api-text);
+}
+.dark .api-reference,
+.dark .api-nav {
+ --api-text: var(--color-gray-100, #e7eaef);
+ --api-muted: var(--color-gray-300, #a9b4c6);
+ --api-line: var(--color-gray-800, #2c333f);
+ --api-surface: var(--color-gray-900, #1e2129);
+ --api-paper: var(--color-background-dark, #10151b);
+ --api-link: var(--color-blue-300, #7ba4f4);
+ --api-selected: #17243b;
+ color-scheme: dark;
+}
+.api-reference {
+ max-width: 1440px;
+ margin: auto;
+ font-size: 15px;
+ line-height: 1.7;
+ overflow-wrap: anywhere;
+}
+.api-reference h1 {
+ font-size: 32px;
+ font-weight: 550;
+ letter-spacing: -0.025em;
+ line-height: 1.25;
+ margin: 28px 0 20px;
+ text-wrap: balance;
+}
+.api-reference h2 {
+ font-size: 20px;
+ font-weight: 550;
+ letter-spacing: -0.015em;
+ line-height: 1.4;
+ margin: 36px 0 14px;
+}
+.api-reference h3,
+.api-reference h4 {
+ font-size: 15px;
+ font-weight: 550;
+ line-height: 1.5;
+ margin: 20px 0 8px;
+}
+.api-reference p {
+ margin: 10px 0;
+ max-width: 72ch;
+}
+.api-reference .prose {
+ color: var(--api-text);
+ font-size: inherit;
+ line-height: inherit;
+ max-width: 72ch;
+}
+.api-reference .prose > :first-child {
+ margin-top: 0;
+}
+.api-reference .prose > :last-child {
+ margin-bottom: 0;
+}
+.api-reference .prose li > p {
+ margin: 6px 0;
+}
+.api-reference .prose :is(h1, h2, h3, h4, h5, h6) > a {
+ color: inherit;
+ text-decoration: none;
+}
+.api-reference .prose :is(h1, h2, h3, h4, h5, h6) > a:hover {
+ text-decoration: underline;
+}
+.api-overview {
+ max-width: 72ch;
+ margin: 32px 0;
+}
+.api-reference .api-overview .prose :is(h3, h4) {
+ font-size: 17px;
+ font-weight: 550;
+ line-height: 1.4;
+ margin: 28px 0 12px;
+}
+.api-reference a,
+.api-nav a {
+ color: var(--api-link);
+ text-underline-offset: 3px;
+}
+.api-reference a:hover {
+ text-decoration: underline;
+}
+.api-reference :is(a, button, input, select, summary):focus-visible,
+.api-nav a:focus-visible {
+ outline: 2px solid var(--api-link);
+ outline-offset: 4px;
+ border-radius: 3px;
+}
+.api-reference code {
+ font-family: var(--api-mono);
+ font-size: 13px;
+ font-weight: 400;
+ overflow-wrap: anywhere;
+}
+.api-reference .prose code::before,
+.api-reference .prose code::after {
+ content: none;
+}
+.api-reference pre {
+ background: var(--api-surface);
+ color: var(--api-text);
+ border: 1px solid var(--api-line);
+ padding: 18px 20px;
+ border-radius: 6px;
+ overflow: auto;
+ font-family: var(--api-mono);
+ font-size: 13px;
+ line-height: 1.75;
+ max-height: 34rem;
+ margin: 14px 0;
+ tab-size: 2;
+}
+.api-reference pre code {
+ font: inherit;
+ padding: 0;
+ background: none;
+ color: inherit;
+}
+.api-reference summary {
+ cursor: pointer;
+ padding: 6px 0;
+ font-size: 13px;
+ color: var(--api-muted);
+}
+.api-reference summary::marker {
+ color: var(--api-muted);
+ font-size: 10px;
+}
+.api-reference summary:hover {
+ color: var(--api-link);
+}
+.api-eyebrow,
+.api-label {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--api-muted);
+}
+.api-reference .api-eyebrow {
+ margin-top: 28px;
+ margin-bottom: -16px;
+}
+.api-lead {
+ font-size: 17px;
+ color: var(--api-muted);
+}
+.api-cards {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 16px;
+ margin: 32px 0;
+}
+.api-card {
+ display: block;
+ border: 1px solid var(--api-line);
+ border-radius: 10px;
+ padding: 26px;
+}
+.api-card:hover {
+ background: var(--api-selected);
+ border-color: var(--api-link);
+ text-decoration: none !important;
+}
+.api-card h2 {
+ margin: 12px 0;
+ color: var(--api-text);
+}
+.api-card p {
+ color: var(--api-muted);
+}
+.api-card > span:last-child {
+ display: inline-block;
+ margin-top: 10px;
+ font-size: 14px;
+}
+.api-crumbs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ align-items: center;
+ color: var(--api-muted);
+ font-size: 13px;
+ margin-bottom: 12px;
+}
+.api-crumbs a {
+ color: var(--api-muted);
+}
+.api-tools {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px 24px;
+ align-items: center;
+ font-size: 13px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid var(--api-line);
+}
+.api-tools > label {
+ margin-right: auto;
+}
+.api-reference label {
+ font-size: 13px;
+ color: var(--api-muted);
+}
+.api-reference select,
+.api-reference input {
+ border: 1px solid var(--api-line);
+ border-radius: 6px;
+ background: var(--api-paper);
+ padding: 8px 10px;
+ color: var(--api-text);
+ font: inherit;
+ font-size: 14px;
+ max-width: 100%;
+}
+.api-reference select {
+ margin-left: 8px;
+}
+.api-reference input::placeholder {
+ color: var(--api-muted);
+}
+.api-connection {
+ max-width: 72ch;
+ margin: 32px 0;
+}
+.api-filter-label {
+ display: block;
+ margin: 16px 0 24px;
+}
+.api-filter-label input {
+ display: block;
+ width: 100%;
+ max-width: 36rem;
+ margin: 8px 0;
+}
+.api-operation-row {
+ display: grid;
+ grid-template-columns: 64px minmax(0, 1.2fr) minmax(0, 1fr);
+ align-items: start;
+ gap: 16px;
+ padding: 16px 8px;
+ border-bottom: 1px solid var(--api-line);
+ font-size: 14px;
+}
+.api-operation-row > span:last-child {
+ color: var(--api-muted);
+}
+.api-operation-row:hover {
+ background: var(--api-selected);
+ text-decoration: none !important;
+}
+.api-method {
+ display: inline-block;
+ font-size: 11px;
+ font-weight: 650;
+ background: var(--api-selected);
+ color: var(--api-link);
+ border-radius: 4px;
+ padding: 4px 8px;
+ line-height: 1.5;
+ white-space: nowrap;
+ width: fit-content;
+ flex-shrink: 0;
+}
+.api-method[data-method="POST"] {
+ background: #ebf9ee;
+ color: #216e4b;
+}
+.api-method[data-method="DELETE"] {
+ background: #fff0ef;
+ color: #ad3039;
+}
+.api-method[data-method="PATCH"],
+.api-method[data-method="PUT"] {
+ background: #fff5e0;
+ color: #865514;
+}
+.dark .api-method[data-method="POST"] {
+ background: #173728;
+ color: #a9e5bd;
+}
+.dark .api-method[data-method="DELETE"] {
+ background: #3d2229;
+ color: #f5b6b6;
+}
+.dark .api-method[data-method="PATCH"],
+.dark .api-method[data-method="PUT"] {
+ background: #392e1c;
+ color: #edcd96;
+}
+.api-signature {
+ display: flex;
+ align-items: baseline;
+ gap: 14px;
+ background: var(--api-surface);
+ border: 1px solid var(--api-line);
+ border-radius: 6px;
+ padding: 14px 18px;
+ margin: 0 0 28px;
+}
+.api-signature code {
+ font-size: 14px;
+}
+.api-operation-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
+ gap: 40px;
+ align-items: start;
+}
+.api-reading {
+ min-width: 0;
+}
+.api-description {
+ margin-bottom: 32px;
+}
+.api-description:empty {
+ display: none;
+}
+.api-request {
+ position: sticky;
+ top: 88px;
+ max-height: calc(100dvh - 112px);
+ overflow: auto;
+ min-width: 0;
+ border: 1px solid var(--api-line);
+ border-radius: 8px;
+ padding: 20px;
+ background: var(--api-surface);
+ font-size: 13px;
+ line-height: 1.65;
+}
+.api-request-heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+}
+.api-request-heading h2 {
+ margin: 0;
+ font-size: 15px;
+ letter-spacing: 0;
+}
+.api-request-heading > span {
+ color: var(--api-muted);
+ font-size: 12px;
+}
+.api-request > p {
+ color: var(--api-muted);
+ margin: 12px 0;
+}
+.api-request pre {
+ background: var(--api-paper);
+ padding: 16px;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+.api-request pre code {
+ word-break: break-all;
+}
+.api-request button {
+ border: 1px solid var(--api-line);
+ background: var(--api-paper);
+ color: var(--api-text);
+ padding: 7px 12px;
+ border-radius: 5px;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+}
+.api-request button:hover {
+ border-color: var(--api-link);
+ color: var(--api-link);
+}
+.api-source-details {
+ border-top: 1px solid var(--api-line);
+ margin-top: 16px;
+ padding-top: 8px;
+}
+.api-field {
+ border-top: 1px solid var(--api-line);
+ padding: 16px 0;
+ margin: 0;
+}
+.api-field h3 {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ align-items: baseline;
+ margin: 0 0 10px;
+}
+.api-field h3 code,
+.api-field > summary code {
+ color: var(--api-text);
+ font-weight: 500;
+}
+.api-field small {
+ font-size: 12px;
+ font-weight: 400;
+ color: var(--api-muted);
+}
+.api-field .api-required {
+ color: var(--api-text);
+ font-size: 12px;
+}
+.api-field .api-field {
+ border-top: 0;
+ border-left: 1px solid var(--api-line);
+ margin: 12px 0 0 4px;
+ padding: 0 0 0 16px;
+}
+.api-type,
+.api-ref,
+.api-constraints {
+ font-size: 13px;
+ color: var(--api-muted);
+}
+.api-variant {
+ border-top: 1px solid var(--api-line);
+ margin-top: 24px;
+ padding-top: 20px;
+}
+.api-variant > h3 {
+ display: flex;
+ gap: 10px;
+ align-items: baseline;
+ margin-top: 0;
+}
+.api-status {
+ border: 1px solid var(--api-line);
+ padding: 1px 6px;
+ border-radius: 4px;
+}
+.api-media {
+ color: var(--api-muted);
+}
+.api-schema-links {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
+ gap: 0 28px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+.api-schema-links li {
+ min-width: 0;
+ border-bottom: 1px solid var(--api-line);
+}
+.api-schema-links a {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ min-height: 44px;
+ padding: 10px 8px;
+}
+.api-schema-links a:hover {
+ background: var(--api-selected);
+}
+.api-nav {
+ padding: 20px 8px;
+ font-size: 14px;
+ line-height: 1.5;
+}
+.api-nav a {
+ display: block;
+ padding: 7px 10px;
+ margin: 2px 0;
+ color: var(--api-muted);
+}
+.api-nav a:hover {
+ color: var(--api-link);
+ background: var(--api-selected);
+}
+.api-nav a[aria-current="page"] {
+ background: var(--api-selected);
+ color: var(--api-link);
+ font-weight: 500;
+ box-shadow: inset 2px 0 var(--api-link);
+}
+.api-nav h2 {
+ font-size: 16px;
+ font-weight: 550;
+ margin: 24px 10px 4px;
+}
+.api-nav .api-nav-version {
+ color: var(--api-muted);
+ font-size: 13px;
+ margin: 0 10px 16px;
+}
+.api-nav h3 {
+ font-size: 13px;
+ font-weight: 550;
+ margin: 26px 10px 8px;
+}
+.api-nav .api-nav-operation {
+ display: grid;
+ grid-template-columns: 42px minmax(0, 1fr);
+ gap: 8px;
+ align-items: baseline;
+}
+.api-nav small {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--api-link);
+}
+.api-nav small[data-method="POST"] {
+ color: #216e4b;
+}
+.api-nav small[data-method="DELETE"] {
+ color: #ad3039;
+}
+.api-nav small[data-method="PATCH"],
+.api-nav small[data-method="PUT"] {
+ color: #865514;
+}
+.dark .api-nav small[data-method="POST"] {
+ color: #a9e5bd;
+}
+.dark .api-nav small[data-method="DELETE"] {
+ color: #f5b6b6;
+}
+.dark .api-nav small[data-method="PATCH"],
+.dark .api-nav small[data-method="PUT"] {
+ color: #edcd96;
+}
+.api-nav .api-nav-back {
+ font-size: 13px;
+ color: var(--api-link);
+}
+.api-reference [hidden] {
+ display: none !important;
+}
+.api-reference[data-api-view="schema"] > .prose {
+ margin-bottom: 24px;
+}
+.api-reference[data-api-view="schema"] > .api-field {
+ max-width: 80ch;
+}
+@media (max-width: 1199px) {
+ .api-operation-grid {
+ grid-template-columns: 1fr;
+ gap: 28px;
+ }
+ .api-request {
+ position: static;
+ max-height: none;
+ overflow: visible;
+ grid-row: 1;
+ }
+}
+@media (max-width: 767px) {
+ main > div:has(> .api-reference) {
+ padding: 24px 20px;
+ }
+ .api-cards {
+ grid-template-columns: 1fr;
+ }
+ .api-reference h1 {
+ font-size: 28px;
+ }
+ .api-operation-row {
+ grid-template-columns: 56px minmax(0, 1fr);
+ gap: 6px 12px;
+ }
+ .api-operation-row > span:last-child {
+ grid-column: 2;
+ }
+ .api-tools {
+ gap: 14px 20px;
+ }
+ .api-tools > label {
+ flex-basis: 100%;
+ }
+ .api-signature {
+ padding: 12px;
+ gap: 10px;
+ }
+ .api-signature code {
+ font-size: 13px;
+ }
+ .api-request {
+ padding: 16px;
+ }
+}
diff --git a/assets/api-reference/reference.js b/assets/api-reference/reference.js
new file mode 100644
index 000000000000..7c51e1843235
--- /dev/null
+++ b/assets/api-reference/reference.js
@@ -0,0 +1,49 @@
+document.querySelectorAll("[data-api-version]").forEach((select) =>
+ select.addEventListener("change", () => {
+ location.href = select.value;
+ }),
+);
+document.querySelectorAll("[data-api-filter]").forEach((input) =>
+ input.addEventListener("input", () => {
+ const term = input.value.toLocaleLowerCase();
+ document.querySelectorAll("[data-api-filter-item]").forEach((row) => {
+ row.hidden = !row.textContent.toLocaleLowerCase().includes(term);
+ });
+ }),
+);
+document.querySelectorAll("[data-api-copy]").forEach((button) =>
+ button.addEventListener("click", async () => {
+ try {
+ await navigator.clipboard.writeText(
+ button.parentElement.querySelector("[data-api-copy-source]")
+ .textContent,
+ );
+ button.textContent = "Copied";
+ } catch {
+ button.textContent = "Select and copy the request";
+ }
+ }),
+);
+document.querySelectorAll("[data-api-example-select]").forEach((select) => {
+ const update = () =>
+ select
+ .closest("[data-api-examples]")
+ .querySelectorAll("[data-api-example]")
+ .forEach((example) => {
+ example.hidden = example.dataset.apiExample !== select.value;
+ });
+ select.addEventListener("change", update);
+ update();
+});
+
+document.querySelectorAll("[data-api-media-select]").forEach((select) =>
+ select.addEventListener("change", () => {
+ document.querySelectorAll("[data-api-media]").forEach((variant) => {
+ variant.hidden = Boolean(
+ select.value &&
+ variant.dataset.apiMedia &&
+ select.value !== variant.dataset.apiMedia,
+ );
+ });
+ }),
+);
diff --git a/content/reference/api/_content.gotmpl b/content/reference/api/_content.gotmpl
new file mode 100644
index 000000000000..d229e946d87a
--- /dev/null
+++ b/content/reference/api/_content.gotmpl
@@ -0,0 +1,22 @@
+{{- $data := index hugo.Data "api-reference" -}}
+{{- if not $data }}
+ {{ errorf "API reference data missing: run ./hack/api-docs/run.sh generate before Hugo" }}
+{{ end -}}
+{{- if ne (int $data.modelVersion) 1 }}
+ {{ errorf "Unsupported API presentation model version" }}
+{{ end -}}
+{{- .AddPage (dict "path" "." "kind" "section" "title" "Docker APIs" "linkTitle" "API reference" "description" "Explore Docker HTTP APIs, connection requirements, and versioned references." "layout" "api-docs" "params" (dict "view" "catalog" "keywords" (slice "API" "HTTP" "OpenAPI"))) -}}
+{{- range $api := $data.apis -}}
+ {{- $path := strings.TrimPrefix "/reference/api/" $api.url | strings.TrimSuffix "/" -}}
+ {{- $aliases := slice -}}
+ {{- if eq $api.id "dvp" }}
+ {{ $aliases = slice "/reference/api/hub/dvp/" }}
+ {{ end -}}
+ {{- $.AddPage (dict "linkTitle" "Latest" "path" $path "url" $api.url "aliases" $aliases "kind" "section" "title" (printf "%s API %v" $api.title $api.version) "description" (printf "%s HTTP API reference, version %v." $api.title $api.version) "layout" "api-docs" "params" (dict "view" "overview" "apiID" $api.id "keywords" (slice "API" $api.product))) -}}
+ {{- range $op := $api.operations -}}
+ {{- $.AddPage (dict "path" (printf "%s/operations/%s" $path (crypto.SHA256 $op.id)) "url" $op.url "title" $op.summary "description" (printf "%s %s — %s API %v." $op.method $op.path $api.title $api.version) "layout" "api-docs" "params" (dict "view" "operation" "apiID" $api.id "operationID" $op.id "keywords" (slice $op.method $op.path $op.id))) -}}
+ {{- end -}}
+ {{- range $schema := $api.schemas -}}
+ {{- $.AddPage (dict "path" (printf "%s/schemas/%s" $path (crypto.SHA256 $schema.name)) "url" $schema.url "title" $schema.name "description" (printf "%s schema in %s API %v." $schema.name $api.title $api.version) "layout" "api-docs" "params" (dict "view" "schema" "apiID" $api.id "schemaName" $schema.name "keywords" (slice "schema" $schema.name))) -}}
+ {{- end -}}
+{{- end -}}
diff --git a/content/reference/api/_index.md b/content/reference/api/_index.md
deleted file mode 100644
index 53ae13d37bd4..000000000000
--- a/content/reference/api/_index.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: API reference
-build:
- render: never
----
-
diff --git a/content/reference/api/dvp/latest.md b/content/reference/api/dvp/latest.md
deleted file mode 100644
index 1ef4fdf075ba..000000000000
--- a/content/reference/api/dvp/latest.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-layout: api
-description: Reference documentation and Swagger (OpenAPI) specification for the Docker Verified Publisher API.
-title: Docker Verified Publisher API reference
-linkTitle: Latest
-weight: 1
-aliases:
- - /reference/api/hub/dvp/
----
diff --git a/content/reference/api/dvp/latest.yaml b/content/reference/api/dvp/latest.yaml
index 01811d55bc7c..292e2fe16c59 100644
--- a/content/reference/api/dvp/latest.yaml
+++ b/content/reference/api/dvp/latest.yaml
@@ -1,4 +1,4 @@
-openapi: 3.0.0
+openapi: 3.2.0
info:
title: DVP Data API
version: 1.0.0
@@ -6,75 +6,79 @@ info:
url: https://docs.docker.com/assets/images/logo-docker-main.png
href: /reference
description: |
- The Docker DVP Data API allows [Docker Verified Publishers](https://docs.docker.com/docker-hub/publish/) to view image pull analytics data for their namespaces. Analytics data can be retrieved in a CSV as raw data, or in a summary format.
-
- #### Summary data
+ The Docker DVP Data API allows [Docker Verified Publishers](https://docs.docker.com/docker-hub/publish/) to view image pull analytics data for their namespaces. Analytics data can be retrieved in a CSV as raw data, or in a summary format.
+
+ #### Summary data
- In your summary data CSV, you will have access to the data points listed below. You can request summary data for a complete week (Monday through Sunday) or for a complete month (available on the first day of the following month).
+ In your summary data CSV, you will have access to the data points listed below. You can request summary data for a complete week (Monday through Sunday) or for a complete month (available on the first day of the following month).
- There are two levels of summary data:
+ There are two levels of summary data:
- - Repository-level, a summary of every namespace and repository
- - Tag- or digest-level, a summary of every namespace, repository, and reference
- (tag or digest)
+ - Repository-level, a summary of every namespace and repository
+ - Tag- or digest-level, a summary of every namespace, repository, and reference
+ (tag or digest)
- The summary data formats contain the following data points:
+ The summary data formats contain the following data points:
- - Unique IP address count
- - Pulls by tag count
- - Pulls by digest count
- - Version check count
+ - Unique IP address count
+ - Pulls by tag count
+ - Pulls by digest count
+ - Version check count
- #### Raw data
+ #### Raw data
- In your raw data CSV you will have access to the data points listed below. You can request raw data for a complete week (Monday through Sunday) or for a complete month (available on the first day of the following month). **Note:** each action is represented as a single row.
-
- - Type (industry)
- - Host (cloud provider)
- - Country (geolocation)
- - Timestamp
- - Namespace
- - Repository
- - Reference (digest is always included, tag is provided when available)
- - HTTP request method
- - Action, one of the following:
- - Pull by tag
- - Pull by digest
- - Version check
- - User-Agent
+ In your raw data CSV you will have access to the data points listed below. You can request raw data for a complete week (Monday through Sunday) or for a complete month (available on the first day of the following month). **Note:** each action is represented as a single row.
+ - Type (industry)
+ - Host (cloud provider)
+ - Country (geolocation)
+ - Timestamp
+ - Namespace
+ - Repository
+ - Reference (digest is always included, tag is provided when available)
+ - HTTP request method
+ - Action, one of the following:
+ - Pull by tag
+ - Pull by digest
+ - Version check
+ - User-Agent
servers:
- url: https://hub.docker.com/api/publisher/analytics/v1
security:
- HubAuth: []
-
-features.openapi:
- schemaDefinitionsTagName: Schemas
-
tags:
- name: authentication
- x-displayName: Authentication Endpoints
+ summary: Authentication Endpoints
+ kind: nav
+ description: Authentication Endpoints reference.
- name: namespaces
- x-displayName: Namespace data
+ summary: Namespace data
+ kind: nav
+ description: Namespace data reference.
- name: discovery
- x-displayName: Discovery
+ summary: Discovery
+ kind: nav
+ description: Discovery reference.
- name: responseDataFile
- x-displayName: ResponseDataFile
description: |
-
+ [ResponseDataFile](#schema-ResponseDataFile)
+ summary: ResponseDataFile
+ kind: info
- name: yearModel
- x-displayName: Year Data Model
description: |
-
+ [YearModel](#schema-YearModel)
+ summary: Year Data Model
+ kind: info
- name: monthModel
- x-displayName: Month Data Model
description: |
-
+ [MonthModel](#schema-MonthModel)
+ summary: Month Data Model
+ kind: info
- name: weekModel
- x-displayName: Week Data Model
description: |
-
-
+ [WeekModel](#schema-WeekModel)
+ summary: Week Data Model
+ kind: info
x-tagGroups:
- name: API
tags:
@@ -87,10 +91,8 @@ x-tagGroups:
- yearModel
- monthModel
- weekModel
-
paths:
/v2/users/login:
- security: []
servers:
- url: https://hub.docker.com
post:
@@ -114,20 +116,19 @@ paths:
description: Login details.
required: true
responses:
- 200:
+ "200":
description: Authentication successful
content:
application/json:
schema:
$ref: "#/components/schemas/PostUsersLoginSuccessResponse"
- 401:
+ "401":
description: Authentication failed or second factor required
content:
application/json:
schema:
$ref: "#/components/schemas/PostUsersLoginErrorResponse"
/v2/users/2fa-login:
- security: []
servers:
- url: https://hub.docker.com
post:
@@ -153,53 +154,73 @@ paths:
description: Login details.
required: true
responses:
- 200:
+ "200":
description: Authentication successful
content:
application/json:
schema:
$ref: "#/components/schemas/PostUsersLoginSuccessResponse"
- 401:
+ "401":
description: Authentication failed or second factor required
content:
application/json:
schema:
$ref: "#/components/schemas/PostUsers2FALoginErrorResponse"
-
-
/:
get:
- tags: [discovery]
+ tags:
+ - discovery
summary: Get namespaces and repos
description: Gets a list of your namespaces and repos which have data available.
operationId: getNamespaces
responses:
- '200':
+ "200":
description: Success
content:
application/json:
schema:
- $ref: '#/components/schemas/NamespaceData'
+ $ref: "#/components/schemas/NamespaceData"
/namespaces:
get:
- tags: [discovery]
+ tags:
+ - discovery
summary: Get user's namespaces
description: Get metadata associated with the namespaces the user has access to, including extra repos associated with the namespaces.
operationId: getUserNamespaces
responses:
- '200':
+ "200":
description: Success
content:
application/json:
schema:
type: array
items:
- $ref: '#/components/schemas/NamespaceMetadata'
- '401':
+ $ref: "#/components/schemas/NamespaceMetadata"
+ examples:
+ publishers:
+ summary: Namespace metadata
+ value:
+ - datasets:
+ - name: pulls
+ timespans:
+ - months
+ - weeks
+ views:
+ - raw
+ - summary
+ - repo-summary
+ - namespace-summary
+ - geo-repo-summary
+ extraRepos: null
+ namespace: org1
+ publisherType: DVP
+ extensionPublisher: false
+ "401":
description: Authentication failed or second factor required
/namespaces/{namespace}:
get:
- tags: [discovery]
+ tags:
+ - discovery
summary: Get namespace
description: Gets metadata associated with specified namespace, including extra repos associated with the namespace.
operationId: getNamespace
@@ -211,15 +232,16 @@ paths:
required: true
description: Namespace to fetch data for
responses:
- '200':
+ "200":
description: Success
content:
application/json:
schema:
- $ref: '#/components/schemas/NamespaceMetadata'
+ $ref: "#/components/schemas/NamespaceMetadata"
/namespaces/{namespace}/pulls:
get:
- tags: [namespaces]
+ tags:
+ - namespaces
summary: Get pull data
description: Gets pulls for the given namespace.
operationId: getNamespacePulls
@@ -233,33 +255,34 @@ paths:
- in: query
name: timespan
schema:
- $ref: '#/components/schemas/TimespanType'
+ $ref: "#/components/schemas/TimespanType"
required: false
description: Timespan type for fetching data
- in: query
name: period
schema:
- $ref: '#/components/schemas/PeriodType'
+ $ref: "#/components/schemas/PeriodType"
required: false
description: Relative period of the period to fetch data
- in: query
name: group
schema:
- $ref: '#/components/schemas/GroupType'
+ $ref: "#/components/schemas/GroupType"
required: false
description: Field to group the data by
responses:
- '200':
+ "200":
description: Success
content:
application/json:
schema:
- $ref: '#/components/schemas/PullData'
- '404':
+ $ref: "#/components/schemas/PullData"
+ "404":
description: Not found - namespace doesn't exist or user does not have permission to access it
/namespaces/{namespace}/repos/{repo}/pulls:
get:
- tags: [namespaces]
+ tags:
+ - namespaces
summary: Get pull data
description: Gets pulls for the given repo.
operationId: getRepoPulls
@@ -279,33 +302,34 @@ paths:
- in: query
name: timespan
schema:
- $ref: '#/components/schemas/TimespanType'
+ $ref: "#/components/schemas/TimespanType"
required: false
description: Timespan type for fetching data
- in: query
name: period
schema:
- $ref: '#/components/schemas/PeriodType'
+ $ref: "#/components/schemas/PeriodType"
required: false
description: Relative period of the period to fetch data
- in: query
name: group
schema:
- $ref: '#/components/schemas/GroupType'
+ $ref: "#/components/schemas/GroupType"
required: false
description: Field to group the data by
responses:
- '200':
+ "200":
description: Success
content:
application/json:
schema:
- $ref: '#/components/schemas/PullData'
- '404':
+ $ref: "#/components/schemas/PullData"
+ "404":
description: Not found - repo doesn't exist or user does not have permission to access it
/namespaces/{namespace}/pulls/exports/years:
get:
- tags: [namespaces]
+ tags:
+ - namespaces
summary: Get years with data
description: Gets a list of years that have data for the given namespace.
operationId: getNamespaceYears
@@ -317,15 +341,16 @@ paths:
required: true
description: Namespace to fetch data for
responses:
- '200':
+ "200":
description: Success
content:
application/json:
schema:
- $ref: '#/components/schemas/YearData'
+ $ref: "#/components/schemas/YearData"
/namespaces/{namespace}/pulls/exports/years/{year}/{timespantype}:
get:
- tags: [namespaces]
+ tags:
+ - namespaces
summary: Get timespans with data
description: Gets a list of timespans of the given type that have data for the given namespace and year.
operationId: getNamespaceTimespans
@@ -345,19 +370,20 @@ paths:
- in: path
name: timespantype
schema:
- $ref: '#/components/schemas/TimespanType'
+ $ref: "#/components/schemas/TimespanType"
required: true
description: Type of timespan to fetch data for
responses:
- '200':
+ "200":
description: Success
content:
application/json:
schema:
- $ref: '#/components/schemas/TimespanData'
+ $ref: "#/components/schemas/TimespanData"
/namespaces/{namespace}/pulls/exports/years/{year}/{timespantype}/{timespan}:
get:
- tags: [namespaces]
+ tags:
+ - namespaces
summary: Get namespace metadata for timespan
description: Gets info about data for the given namespace and timespan.
operationId: getNamespaceTimespanMetadata
@@ -377,7 +403,7 @@ paths:
- in: path
name: timespantype
schema:
- $ref: '#/components/schemas/TimespanType'
+ $ref: "#/components/schemas/TimespanType"
required: true
description: Type of timespan to fetch data for
- in: path
@@ -387,17 +413,18 @@ paths:
required: true
description: Timespan to fetch data for
responses:
- '200':
+ "200":
description: Success
content:
application/json:
schema:
- $ref: '#/components/schemas/TimespanModel'
- '404':
+ $ref: "#/components/schemas/TimespanModel"
+ "404":
description: Not Found
/namespaces/{namespace}/pulls/exports/years/{year}/{timespantype}/{timespan}/{dataview}:
get:
- tags: [namespaces]
+ tags:
+ - namespaces
summary: Get namespace data for timespan
description: Gets a list of URLs that can be used to download the pull data for the given namespace and timespan.
operationId: getNamespaceDataByTimespan
@@ -417,7 +444,7 @@ paths:
- in: path
name: timespantype
schema:
- $ref: '#/components/schemas/TimespanType'
+ $ref: "#/components/schemas/TimespanType"
required: true
description: Type of timespan to fetch data for
- in: path
@@ -429,19 +456,28 @@ paths:
- in: path
name: dataview
schema:
- $ref: '#/components/schemas/DataviewType'
+ $ref: "#/components/schemas/DataviewType"
required: true
description: Type of data to fetch
responses:
- '200':
+ "200":
description: Success
content:
application/json:
schema:
- $ref: '#/components/schemas/ResponseData'
+ $ref: "#/components/schemas/ResponseData"
+ examples:
+ download:
+ summary: Illustrative download location
+ description: Use the signed URL returned by the API. The URL and file size in this example are illustrative.
+ value:
+ data:
+ - url: https://example.invalid/exports/pulls.csv.gz?X-Amz-Expires=21600&X-Amz-Signature=EXAMPLE
+ size: 1024
/repos/pulls:
get:
- tags: [namespaces]
+ tags:
+ - namespaces
summary: Get pull data for multiple repos
description: Gets pull for the given repos.
operationId: getManyReposPulls
@@ -457,32 +493,34 @@ paths:
- in: query
name: timespan
schema:
- $ref: '#/components/schemas/TimespanType'
+ $ref: "#/components/schemas/TimespanType"
required: false
description: Timespan type for fetching data
- in: query
name: period
schema:
- $ref: '#/components/schemas/PeriodType'
+ $ref: "#/components/schemas/PeriodType"
required: false
description: Relative period of the period to fetch data
- in: query
name: group
schema:
- $ref: '#/components/schemas/GroupType'
+ $ref: "#/components/schemas/GroupType"
required: false
description: Field to group the data by
responses:
- '200':
+ "200":
description: Success
content:
application/json:
schema:
- $ref: '#/components/schemas/ReposPullData'
-
+ $ref: "#/components/schemas/ReposPullData"
components:
schemas:
UsersLoginRequest:
+ examples:
+ - username: myusername
+ password: hunter2
description: User login details
type: object
required:
@@ -494,12 +532,12 @@ components:
type: string
example: myusername
password:
- description:
- The password or personal access token (PAT) of the Docker Hub
- account to authenticate with.
+ description: The password or personal access token (PAT) of the Docker Hub account to authenticate with.
type: string
example: hunter2
PostUsersLoginSuccessResponse:
+ examples:
+ - token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
description: successful user login response
type: object
properties:
@@ -510,8 +548,9 @@ components:
This token can be used in the HTTP Authorization header as a JWT to authenticate with the Docker Hub APIs.
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- nullable: false
PostUsersLoginErrorResponse:
+ examples:
+ - detail: Incorrect authentication credentials
description: failed user login response or second factor required
type: object
required:
@@ -521,16 +560,16 @@ components:
description: Description of the error.
type: string
example: Incorrect authentication credentials
- nullable: false
login_2fa_token:
- description:
- Short-lived token to be used on `/v2/users/2fa-login` to
- complete the authentication. This field is present only if 2FA is
- enabled.
- type: string
+ description: Short-lived token to be used on `/v2/users/2fa-login` to complete the authentication. This field is present only if 2FA is enabled.
+ type:
+ - string
+ - "null"
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- nullable: true
Users2FALoginRequest:
+ examples:
+ - login_2fa_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
+ code: "123456"
description: Second factor user login details
type: object
required:
@@ -542,12 +581,12 @@ components:
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
code:
- description:
- The Time-based One-Time Password of the Docker Hub account to
- authenticate with.
+ description: The Time-based One-Time Password of the Docker Hub account to authenticate with.
type: string
- example: 123456
+ example: "123456"
PostUsers2FALoginErrorResponse:
+ examples:
+ - detail: Incorrect authentication credentials
description: failed second factor login response.
type: object
properties:
@@ -555,138 +594,299 @@ components:
description: Description of the error.
type: string
example: Incorrect authentication credentials
- nullable: false
-
ResponseData:
properties:
data:
type: array
- description: |
- List of urls to download the data. When the data is large, the data will be split into multiple files.
- items:
- $ref: '#/components/schemas/ResponseDataFile'
+ description: Download URLs and file sizes for the export.
+ items:
+ $ref: "#/components/schemas/ResponseDataFile"
+ type: object
+ description: Download locations for the selected export. Large exports can span multiple files.
ResponseDataFile:
properties:
url:
type: string
+ format: uri
+ description: Signed download URL. URLs expire six hours after generation.
size:
type: integer
format: int64
+ description: Size of the file in bytes.
+ type: object
NamespaceData:
+ examples:
+ - namespaces:
+ - myorganization
properties:
namespaces:
type: array
items:
type: string
+ type: object
NamespaceMetadata:
properties:
namespace:
type: string
extraRepos:
- type: array
+ type:
+ - array
+ - "null"
items:
type: string
+ description: Additional repositories associated with the namespace. Null when none are configured.
datasets:
type: array
items:
- $ref: '#/components/schemas/DatasetModel'
+ $ref: "#/components/schemas/DatasetModel"
+ publisherType:
+ type: string
+ description: Publisher program associated with the namespace.
+ extensionPublisher:
+ type: boolean
+ description: Whether the namespace is an extension publisher.
+ type: object
+ examples:
+ - datasets:
+ - name: pulls
+ timespans:
+ - months
+ - weeks
+ views:
+ - raw
+ - summary
+ - repo-summary
+ - namespace-summary
+ - geo-repo-summary
+ extraRepos: null
+ namespace: org1
+ publisherType: DVP
+ extensionPublisher: false
+ - datasets:
+ - name: pulls
+ timespans:
+ - months
+ - weeks
+ views:
+ - repo-summary
+ - namespace-summary
+ - name: extensions
+ timespans:
+ - months
+ - weeks
+ views:
+ - ext-summary
+ extraRepos: null
+ namespace: org4
+ publisherType: DVP
+ extensionPublisher: true
DatasetModel:
properties:
name:
- $ref: '#/components/schemas/DatasetType'
+ $ref: "#/components/schemas/DatasetType"
views:
type: array
items:
- $ref: '#/components/schemas/DataviewType'
+ $ref: "#/components/schemas/DataviewType"
timespans:
type: array
items:
- $ref: '#/components/schemas/TimespanType'
+ $ref: "#/components/schemas/TimespanType"
+ type: object
+ description: Datasets and views available to the publisher, based on its entitlements.
PullData:
properties:
pulls:
- type: array
+ type:
+ - array
+ - "null"
items:
- $ref: '#/components/schemas/PullModel'
+ $ref: "#/components/schemas/PullModel"
+ description: Pull statistics for the selected interval. Null when no records are returned.
+ type: object
+ examples:
+ - pulls:
+ - end: "2022-08-07T00:00:00Z"
+ pullCount: 11
+ repo: neo4j-admin
+ start: "2022-08-01T00:00:00Z"
+ ipCount: 10
+ - end: "2022-08-14T00:00:00Z"
+ pullCount: 11
+ repo: neo4j-admin
+ start: "2022-08-08T00:00:00Z"
+ ipCount: 10
+ - pulls: null
ReposPullData:
properties:
repos:
type: object
additionalProperties:
- $ref: '#/components/schemas/PullData'
+ $ref: "#/components/schemas/PullData"
+ type: object
+ examples:
+ - repos:
+ org2/neo4j-admin:
+ pulls:
+ - end: "2022-08-07T00:00:00Z"
+ pullCount: 11
+ repo: neo4j-admin
+ start: "2022-08-01T00:00:00Z"
+ ipCount: 10
+ - end: "2022-08-14T00:00:00Z"
+ pullCount: 11
+ repo: neo4j-admin
+ start: "2022-08-08T00:00:00Z"
+ ipCount: 10
+ org2/neo4j:
+ pulls:
+ - end: "2022-08-07T00:00:00Z"
+ pullCount: 272407
+ repo: neo4j
+ start: "2022-08-01T00:00:00Z"
+ ipCount: 41490
+ - end: "2022-08-14T00:00:00Z"
+ pullCount: 272407
+ repo: neo4j
+ start: "2022-08-08T00:00:00Z"
+ ipCount: 41490
PullModel:
properties:
start:
type: string
+ format: date-time
+ description: Start of the reporting interval in RFC 3339 format.
end:
type: string
+ format: date-time
+ description: End of the reporting interval in RFC 3339 format.
repo:
type: string
+ description: Repository name. Present for results grouped by repository.
namespace:
type: string
+ description: Namespace name. Present for results grouped by namespace.
pullCount:
type: integer
ipCount:
type: integer
country:
type: string
-
+ description: Country. Present for results grouped by geography and repository.
+ type: object
YearData:
+ examples:
+ - years:
+ - year: 2025
properties:
years:
type: array
items:
- $ref: '#/components/schemas/YearModel'
+ $ref: "#/components/schemas/YearModel"
+ type: object
YearModel:
properties:
year:
type: integer
+ type: object
MonthData:
properties:
months:
type: array
items:
- $ref: '#/components/schemas/MonthModel'
+ $ref: "#/components/schemas/MonthModel"
+ type: object
+ required:
+ - months
+ examples:
+ - months:
+ - month: 5
+ - month: 7
MonthModel:
properties:
month:
type: integer
+ type: object
+ required:
+ - month
+ examples:
+ - month: 7
WeekData:
properties:
weeks:
type: array
items:
- $ref: '#/components/schemas/WeekModel'
+ $ref: "#/components/schemas/WeekModel"
+ type: object
+ required:
+ - weeks
+ examples:
+ - weeks:
+ - week: 31
+ - week: 32
WeekModel:
properties:
week:
type: integer
+ type: object
+ required:
+ - week
+ examples:
+ - week: 31
TimespanType:
type: string
- enum: [months,weeks]
+ enum:
+ - months
+ - weeks
PeriodType:
type: string
- enum: [last-2-months,last-3-months,last-6-months,last-12-months]
+ enum:
+ - last-2-months
+ - last-3-months
+ - last-6-months
+ - last-12-months
DataviewType:
type: string
- enum: [raw,summary,repo-summary,namespace-summary]
+ enum:
+ - raw
+ - summary
+ - repo-summary
+ - namespace-summary
+ - geo-repo-summary
+ - ext-summary
+ - ext-premium-summary
+ - ext-premium-geo
DatasetType:
type: string
- enum: [pulls]
+ enum:
+ - pulls
+ - extensions
TimespanModel:
oneOf:
- - $ref: '#/components/schemas/MonthModel'
- - $ref: '#/components/schemas/WeekModel'
+ - $ref: "#/components/schemas/MonthModel"
+ - $ref: "#/components/schemas/WeekModel"
+ examples:
+ - month: 7
+ - week: 31
TimespanData:
oneOf:
- - $ref: '#/components/schemas/MonthData'
- - $ref: '#/components/schemas/WeekData'
+ - $ref: "#/components/schemas/MonthData"
+ - $ref: "#/components/schemas/WeekData"
+ examples:
+ - months:
+ - month: 5
+ - month: 7
+ - weeks:
+ - week: 31
+ - week: 32
GroupType:
type: string
- enum: [repo,namespace]
+ enum:
+ - repo
+ - namespace
securitySchemes:
HubAuth:
- type: https
+ type: http
scheme: bearer
bearerFormat: JWT
description: |
@@ -694,3 +894,6 @@ components:
This authentication documentation is duplicated from the [Hub API Authentication docs](https://docs.docker.com/reference/api/hub/latest/#tag/authentication)
x-displayName: Docker Hub Authentication
+jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
+x-features-openapi:
+ schemaDefinitionsTagName: Schemas
diff --git a/content/reference/api/hub/latest.md b/content/reference/api/hub/latest.md
deleted file mode 100644
index 15fd57a3db7f..000000000000
--- a/content/reference/api/hub/latest.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-layout: api
-description: Reference documentation and Swagger (OpenAPI) specification for the Docker Hub API.
-title: Docker Hub API reference
-linkTitle: Latest
-weight: 1
----
diff --git a/content/reference/api/hub/latest.yaml b/content/reference/api/hub/latest.yaml
index 3ed32a2c6302..05ce1e7e3de6 100644
--- a/content/reference/api/hub/latest.yaml
+++ b/content/reference/api/hub/latest.yaml
@@ -1,6 +1,4 @@
-# yaml-language-server: $schema=https://raw.githubusercontent.com/OAI/OpenAPI-Specification/refs/heads/main/schemas/v3.0/schema.yaml
-
-openapi: 3.0.3
+openapi: 3.2.0
info:
title: Docker HUB API
version: 2-beta
@@ -21,16 +19,17 @@ servers:
url: https://hub.docker.com
tags:
- name: changelog
- x-displayName: Changelog
description: |
See the [Changelog](/reference/api/hub/changelog) for a summary of changes across Docker Hub API versions.
+ summary: Changelog
+ kind: info
- name: resources
- x-displayName: Resources
description: |
The following resources are available to interact with the documented API:
- [Docker Hub CLI tool](https://github.com/docker/hub-tool#readme) (currently experimental)
+ summary: Resources
+ kind: info
- name: rate-limiting
- x-displayName: Rate Limiting
description: |
The Docker Hub API is limited on the amount of requests you can perform per minute against it.
@@ -46,9 +45,10 @@ tags:
**Note**: These rate limits are separate from anti-abuse and Docker Hub download, or pull rate limiting.
To learn more about Docker Hub pull rate limiting, see [Usage and limits](https://docs.docker.com/docker-hub/usage/).
+ summary: Rate Limiting
+ kind: info
- name: authentication
- x-displayName: Authentication
- description: |
+ description: |+
Most Docker Hub API endpoints require you to authenticate using your Docker credentials before using them.
Additionally, similar to the Docker Hub UI features, API endpoint responses may vary depending on your subscription (Personal, Pro, or Team) and your account's permissions.
@@ -90,16 +90,18 @@ tags:
These labels will show up on routes in this reference that allow for use of bearer
tokens issued from them.
-
-
+
+
+ summary: Authentication
+ kind: info
- name: authentication-api
- x-displayName: Authentication
description: |
The authentication endpoints allow you to authenticate with Docker Hub APIs.
For more information, see [Authentication](#tag/authentication).
+ summary: Authentication
+ kind: nav
- name: access-tokens
- x-displayName: Personal Access Tokens
description: |
The Personal Access Token endpoints lets you manage personal access tokens. For more information, see [Access Tokens](https://docs.docker.com/security/access-tokens/personal-access-tokens/).
@@ -112,50 +114,58 @@ tags:
If you were to define both `repo:write` *and* `repo:read`, then `repo:read` is assumed by `repo:write` and ignored.
***Treat your personal access token like your password and keep it secret. You cannot retrieve your token after it is generated.***
+ summary: Personal Access Tokens
+ kind: nav
- name: audit-logs
- x-displayName: Audit Logs
description: |
The Audit Logs API endpoints allow you to query audit log events across a namespace.
For more information, see [Audit Logs](https://docs.docker.com/admin/activity-logs/).
+ summary: Audit Logs
+ kind: nav
- name: org-settings
- x-displayName: Org Settings
description: |
The Org Settings API endpoints allow you to manage your organization's settings.
+ summary: Org Settings
+ kind: nav
- name: repositories
- x-displayName: Repositories
description: |
The repository endpoints allow you to access your repository's tags.
+ summary: Repositories
+ kind: nav
- name: orgs
- x-displayName: Organizations
x-audience: public
description: |
The organization endpoints allow you to interact with and manage your organizations.
For more information, see [Organization administration overview](https://docs.docker.com/admin/organization/).
+ summary: Organizations
+ kind: nav
- name: groups
- x-displayName: Groups (Teams)
x-audience: public
description: |
The groups endpoints allow you to manage your organization's teams and their members.
For more information, see [Create and manage a team](https://docs.docker.com/admin/organization/manage/manage-a-team/).
+ summary: Groups (Teams)
+ kind: nav
- name: invites
- x-displayName: Invites
x-audience: public
description: |
The invites endpoints allow you to manage invites for users to join your Docker organization.
For more information, see [Invite members](https://docs.docker.com/admin/organization/manage/members/#invite-members).
+ summary: Invites
+ kind: nav
- name: scim
- x-displayName: SCIM
x-audience: public
description: |
SCIM is a provisioning system that lets you manage users within your identity provider (IdP).
For more information, see [System for Cross-domain Identity management](https://docs.docker.com/security/for-admins/provisioning/scim/).
+ summary: SCIM
+ kind: nav
- name: org-access-tokens
- x-displayName: Organization Access Tokens
x-audience: public
description: |
The organization access token endpoints allow you to manage organization access tokens (OATs). See [Organization access tokens](https://docs.docker.com/security/for-admins/access-tokens/) for more information.
@@ -165,6 +175,8 @@ tags:
- `GET /v2/repositories/{namespace}/{repository}` — use [Get repository](#tag/repositories/operation/GetRepository) instead.
- `GET /v2/repositories/{namespace}` — use [List repositories](#tag/repositories/operation/listNamespaceRepositories) instead.
- `GET /v2/users/{username}/repositories` — use [List repositories](#tag/repositories/operation/listNamespaceRepositories) instead.
+ summary: Organization Access Tokens
+ kind: nav
paths:
/v2/users/login:
post:
@@ -174,16 +186,16 @@ paths:
operationId: PostUsersLogin
security: []
deprecated: true
- description: |
+ description: |+
Creates and returns a bearer token in JWT format that you can use to authenticate with Docker Hub APIs.
The returned token is used in the HTTP Authorization header like `Authorization: Bearer {TOKEN}`.
_**As of September 16, 2024, this route requires a personal access token (PAT) instead of a password if your organization has SSO enforced.**_
-
+
+ Deprecated: Use [Create access token](#tag/authentication-api/operation/AuthCreateAccessToken) instead.
+
requestBody:
content:
application/json:
@@ -257,6 +269,9 @@ paths:
content:
application/json:
schema:
+ examples:
+ - identifier: myusername
+ secret: dckr_pat_124509ugsdjga93
description: Request to create access token
type: object
required:
@@ -310,6 +325,7 @@ paths:
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
+ operationId: postV2AccessTokens
get:
summary: List personal access tokens
description: Returns a paginated list of personal access tokens.
@@ -318,12 +334,14 @@ paths:
security:
- bearerAuth: []
parameters:
- - in: query
+ - description: Page number to return.
+ in: query
name: page
schema:
type: number
default: 1
- - in: query
+ - description: Number of results per page.
+ in: query
name: page_size
schema:
type: number
@@ -339,9 +357,11 @@ paths:
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
+ operationId: getV2AccessTokens
/v2/access-tokens/{uuid}:
parameters:
- - in: path
+ - description: UUID of the personal access token.
+ in: path
name: uuid
required: true
schema:
@@ -371,6 +391,7 @@ paths:
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
+ operationId: patchV2AccessTokensByUuid
get:
summary: Get personal access token
description: Returns a personal access token by UUID.
@@ -384,17 +405,32 @@ paths:
content:
application/json:
schema:
+ examples:
+ - uuid: b30bbf97-506c-4ecd-aabc-842f3cb484fb
+ client_id: HUB
+ creator_ip: 127.0.0.1
+ creator_ua: some user agent
+ created_at: "2021-07-20T12:00:00.000000Z"
+ last_used: null
+ generated_by: manual
+ is_active: true
+ token: "***"
+ token_label: My read only token
+ scopes:
+ - repo:read
+ expires_at: "2021-10-28T18:30:19.520861Z"
allOf:
- $ref: "#/components/schemas/accessToken"
- type: object
properties:
token:
type: string
- example: ""
+ example: "***"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
+ operationId: getV2AccessTokensByUuid
delete:
summary: Delete personal access token
description: |
@@ -410,13 +446,14 @@ paths:
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
+ operationId: deleteV2AccessTokensByUuid
/v2/auditlogs/{account}/actions:
get:
summary: List audit log actions
- description: |
+ description: |+
List audit log actions for a namespace to be used as a filter for querying audit log events.
-
+
operationId: AuditLogs_ListAuditActions
security:
- bearerAuth: []
@@ -688,13 +725,26 @@ paths:
description: ""
content:
application/json:
- schema: {}
+ schema:
+ $ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: internal error
+ errinfo: null
default:
description: An unexpected error response.
content:
application/json:
schema:
- $ref: "#/components/schemas/rpcStatus"
+ $ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: internal error
+ errinfo: null
parameters:
- name: account
description: Namespace to query audit log actions for.
@@ -707,10 +757,10 @@ paths:
/v2/auditlogs/{account}:
get:
summary: List audit log events
- description: |
+ description: |+
List audit log events for a given namespace.
-
+
operationId: AuditLogs_ListAuditLogs
security:
- bearerAuth: []
@@ -732,7 +782,7 @@ paths:
data:
digest: sha256:c1ae9c435032a276f80220c7d9b40f76266bbe79243d34f9cda30b76fe114dfa
tag: latest
- timestamp: "2021-02-19T01:34:35Z"
+ timestamp: 2021-02-19T01:34:35Z
action_description: |
pushed the tag latest with the digest sha256:c1ae9c435032a to the repository docker/example
- account: docker
@@ -742,12 +792,12 @@ paths:
data:
lease_id: l_3EgPuRCjtUqT279CFPOQWcO8zOf
resource_type: run_4cpu_8mem
- started_at: "2026-06-04T18:24:21Z"
- updated_at: "2026-06-04T18:36:43Z"
+ started_at: 2026-06-04T18:24:21Z
+ updated_at: 2026-06-04T18:36:43Z
org_id: b908ca6e-b9a9-4a53-a9a5-6bec96f72432
user_id: ecae6747-e42c-43cb-925d-cfce1ab32b02
- timestamp: "2026-06-04T18:36:43Z"
- action_description: "offload lease 'l_3EgPuRCjtUqT279CFPOQWcO8zOf' ended, ran for '12m22s'"
+ timestamp: 2026-06-04T18:36:43Z
+ action_description: offload lease 'l_3EgPuRCjtUqT279CFPOQWcO8zOf' ended, ran for '12m22s'
"429":
description: ""
content:
@@ -762,13 +812,26 @@ paths:
description: ""
content:
application/json:
- schema: {}
+ schema:
+ $ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: internal error
+ errinfo: null
default:
description: An unexpected error response.
content:
application/json:
schema:
- $ref: "#/components/schemas/rpcStatus"
+ $ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: internal error
+ errinfo: null
parameters:
- name: account
description: Namespace to query audit logs for.
@@ -858,6 +921,7 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
+ operationId: getV2OrgsByNameSettings
put:
summary: Update organization settings
description: |
@@ -875,6 +939,11 @@ paths:
content:
application/json:
schema:
+ examples:
+ - restricted_images:
+ enabled: true
+ allow_official_images: true
+ allow_verified_publishers: true
required:
- restricted_images
properties:
@@ -900,6 +969,7 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
+ operationId: putV2OrgsByNameSettings
/v2/orgs/{name}/access-tokens:
post:
summary: Create access token
@@ -930,6 +1000,7 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
+ operationId: postV2OrgsByNameAccessTokens
get:
summary: List access tokens
description: |
@@ -939,12 +1010,14 @@ paths:
security:
- bearerAuth: []
parameters:
- - in: query
+ - description: Page number to return.
+ in: query
name: page
schema:
type: number
default: 1
- - in: query
+ - description: Number of results per page.
+ in: query
name: page_size
schema:
type: number
@@ -962,7 +1035,7 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
-
+ operationId: getV2OrgsByNameAccessTokens
/v2/orgs/{org_name}/access-tokens/{access_token_id}:
parameters:
- $ref: "#/components/parameters/org_name"
@@ -972,7 +1045,7 @@ paths:
schema:
type: string
description: The ID of the access token to retrieve
- example: "a7a5ef25-8889-43a0-8cc7-f2a94268e861"
+ example: a7a5ef25-8889-43a0-8cc7-f2a94268e861
get:
summary: Get access token
description: |
@@ -994,6 +1067,7 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
+ operationId: getV2OrgsByOrgNameAccessTokensByAccessTokenId
patch:
summary: Update access token
description: |
@@ -1021,6 +1095,7 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
+ operationId: patchV2OrgsByOrgNameAccessTokensByAccessTokenId
delete:
summary: Delete access token
description: |
@@ -1038,6 +1113,7 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
+ operationId: deleteV2OrgsByOrgNameAccessTokensByAccessTokenId
/v2/namespaces/{namespace}/repositories/{repository}/tags:
parameters:
- $ref: "#/components/parameters/namespace"
@@ -1045,10 +1121,10 @@ paths:
get:
operationId: ListRepositoryTags
summary: List repository tags
- description: |
+ description: |+
Returns the list of tags for the specified repository.
-
+
tags:
- repositories
security:
@@ -1075,10 +1151,10 @@ paths:
$ref: "#/components/responses/NotFound"
head:
summary: Check repository tags
- description: |
+ description: |+
Checks whether the repository has any tags.
-
+
tags:
- repositories
security:
@@ -1090,6 +1166,7 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
+ operationId: headV2NamespacesByNamespaceRepositoriesByRepositoryTags
/v2/namespaces/{namespace}/repositories/{repository}/tags/{tag}:
parameters:
- $ref: "#/components/parameters/namespace"
@@ -1098,10 +1175,10 @@ paths:
get:
operationId: GetRepositoryTag
summary: Read repository tag
- description: |
+ description: |+
Returns details for a specific tag in the specified repository.
-
+
tags:
- repositories
security:
@@ -1115,10 +1192,10 @@ paths:
$ref: "#/components/responses/NotFound"
head:
summary: Check repository tag
- description: |
+ description: |+
Checks whether the specified tag exists in the repository.
-
+
tags:
- repositories
security:
@@ -1130,19 +1207,20 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
+ operationId: headV2NamespacesByNamespaceRepositoriesByRepositoryTagsByTag
/v2/namespaces/{namespace}/repositories/{repository}/immutabletags:
parameters:
- $ref: "#/components/parameters/namespace"
- $ref: "#/components/parameters/repository"
patch:
operationId: UpdateRepositoryImmutableTags
- summary: "Update repository immutable tags"
- description: |
+ summary: Update repository immutable tags
+ description: |+
Updates the immutable tags configuration for this repository.
**Only users with administrative privileges for the repository can modify these settings.**
-
+
tags:
- repositories
security:
@@ -1150,15 +1228,15 @@ paths:
requestBody:
$ref: "#/components/requestBodies/update_repository_immutable_tags_request"
responses:
- 200:
+ "200":
$ref: "#/components/responses/update_repository_immutable_tags_response"
- 400:
+ "400":
$ref: "#/components/responses/bad_request"
- 401:
+ "401":
$ref: "#/components/responses/unauthorized"
- 403:
+ "403":
$ref: "#/components/responses/forbidden"
- 404:
+ "404":
$ref: "#/components/responses/not_found"
/v2/namespaces/{namespace}/repositories/{repository}/immutabletags/verify:
parameters:
@@ -1166,13 +1244,13 @@ paths:
- $ref: "#/components/parameters/repository"
post:
operationId: VerifyRepositoryImmutableTags
- summary: "Verify repository immutable tags"
- description: |
+ summary: Verify repository immutable tags
+ description: |+
Validates the immutable tags regex passed in the request body and returns a list of tags matching it in this repository.
**Only users with administrative privileges for the repository can call this endpoint.**
-
+
tags:
- repositories
security:
@@ -1180,15 +1258,15 @@ paths:
requestBody:
$ref: "#/components/requestBodies/immutable_tags_verify_request"
responses:
- 200:
+ "200":
$ref: "#/components/responses/immutable_tags_verify_response"
- 400:
+ "400":
$ref: "#/components/responses/bad_request"
- 401:
+ "401":
$ref: "#/components/responses/unauthorized"
- 403:
+ "403":
$ref: "#/components/responses/forbidden"
- 404:
+ "404":
$ref: "#/components/responses/not_found"
/v2/repositories/{namespace}/{repository}/groups:
parameters:
@@ -1196,10 +1274,10 @@ paths:
- $ref: "#/components/parameters/repository"
post:
summary: Assign a group (Team) to a repository for access
- description: |
+ description: |+
Assigns an organization group (team) to a repository with a specified permission level.
-
+
tags:
- repositories
operationId: CreateRepositoryGroup
@@ -1213,7 +1291,7 @@ paths:
$ref: "#/components/schemas/RepositoryGroupCreationRequest"
example:
group_id: 12345
- permission: "write"
+ permission: write
responses:
"200":
description: Repository group permission created successfully
@@ -1222,8 +1300,8 @@ paths:
schema:
$ref: "#/components/schemas/RepositoryGroup"
example:
- group_name: "developers"
- permission: "write"
+ group_name: developers
+ permission: write
group_id: 12345
"400":
description: Bad Request - Invalid request parameters
@@ -1231,6 +1309,12 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: bad request
+ errinfo: null
"401":
$ref: "#/components/responses/unauthorized"
"403":
@@ -1248,14 +1332,14 @@ paths:
Public repositories are accessible to everyone, while private repositories require appropriate authentication and permissions.
-
+
**OAT listing behavior**: an OAT with the `scope-repository-list` scope sees all repositories including private ones. An OAT without that scope only sees public repositories. This filtering is silent: the response is a normal `200` with no indication that private repositories were withheld.
tags:
- repositories
security:
- bearerAuth: []
- - {} # Allow anonymous access for public repositories
+ - {}
parameters:
- in: query
name: page
@@ -1309,59 +1393,59 @@ paths:
repositories_list:
value:
count: 287
- next: "https://hub.docker.com/v2/namespaces/docker/repositories?page=2&page_size=2"
+ next: https://hub.docker.com/v2/namespaces/docker/repositories?page=2&page_size=2
previous: null
results:
- - name: "highland_builder"
- namespace: "docker"
- repository_type: "image"
+ - name: highland_builder
+ namespace: docker
+ repository_type: image
status: 1
- status_description: "active"
- description: "Image for performing Docker build requests"
+ status_description: active
+ description: Image for performing Docker build requests
is_private: false
star_count: 7
pull_count: 15722123
- last_updated: "2023-06-20T10:44:45.459826Z"
- last_modified: "2024-10-16T13:48:34.145251Z"
- date_registered: "2015-05-19T21:13:35.937763Z"
+ last_updated: 2023-06-20T10:44:45.459826Z
+ last_modified: 2024-10-16T13:48:34.145251Z
+ date_registered: 2015-05-19T21:13:35.937763Z
affiliation: ""
media_types:
- - "application/octet-stream"
- - "application/vnd.docker.container.image.v1+json"
- - "application/vnd.docker.distribution.manifest.v1+prettyjws"
+ - application/octet-stream
+ - application/vnd.docker.container.image.v1+json
+ - application/vnd.docker.distribution.manifest.v1+prettyjws
content_types:
- - "unrecognized"
- - "image"
+ - unrecognized
+ - image
categories:
- - name: "Languages & frameworks"
- slug: "languages-and-frameworks"
- - name: "Integration & delivery"
- slug: "integration-and-delivery"
- - name: "Operating systems"
- slug: "operating-systems"
+ - name: Languages & frameworks
+ slug: languages-and-frameworks
+ - name: Integration & delivery
+ slug: integration-and-delivery
+ - name: Operating systems
+ slug: operating-systems
storage_size: 488723114800
- - name: "whalesay"
- namespace: "docker"
+ - name: whalesay
+ namespace: docker
repository_type: null
status: 1
- status_description: "active"
- description: "An image for use in the Docker demo tutorial"
+ status_description: active
+ description: An image for use in the Docker demo tutorial
is_private: false
star_count: 757
pull_count: 130737682
- last_updated: "2015-06-19T19:06:27.388123Z"
- last_modified: "2024-10-16T13:48:34.145251Z"
- date_registered: "2015-06-09T18:16:36.527329Z"
+ last_updated: 2015-06-19T19:06:27.388123Z
+ last_modified: 2024-10-16T13:48:34.145251Z
+ date_registered: 2015-06-09T18:16:36.527329Z
affiliation: ""
media_types:
- - "application/vnd.docker.distribution.manifest.v1+prettyjws"
+ - application/vnd.docker.distribution.manifest.v1+prettyjws
content_types:
- - "image"
+ - image
categories:
- - name: "Languages & frameworks"
- slug: "languages-and-frameworks"
- - name: "Integration & delivery"
- slug: "integration-and-delivery"
+ - name: Languages & frameworks
+ slug: languages-and-frameworks
+ - name: Integration & delivery
+ slug: integration-and-delivery
storage_size: 103666708
"400":
description: Bad Request - Invalid request parameters
@@ -1375,10 +1459,8 @@ paths:
value:
fields:
ordering:
- [
- "Invalid ordering value. Must be one of: name, -name, last_updated, -last_updated, pull_count, -pull_count",
- ]
- text: "Invalid ordering value"
+ - "Invalid ordering value. Must be one of: name, -name, last_updated, -last_updated, pull_count, -pull_count"
+ text: Invalid ordering value
"401":
$ref: "#/components/responses/unauthorized"
"403":
@@ -1389,13 +1471,19 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: not found
+ errinfo: null
post:
summary: Create a new repository
- description: |
+ description: |+
Creates a new repository within the specified namespace. The repository will be created
with the provided metadata including name, description, and privacy settings.
-
+
operationId: CreateRepository
tags:
- repositories
@@ -1408,26 +1496,26 @@ paths:
schema:
$ref: "#/components/schemas/repo_creation_request"
example:
- name: "my-app"
- namespace: "myorganization"
- description: "A sample application repository"
- full_description: "This is a comprehensive description of my application repository that contains additional details about the project."
- registry: "docker.io"
+ name: my-app
+ namespace: myorganization
+ description: A sample application repository
+ full_description: This is a comprehensive description of my application repository that contains additional details about the project.
+ registry: docker.io
is_private: false
responses:
- 201:
+ "201":
description: Repository created successfully
content:
application/json:
schema:
$ref: "#/components/schemas/repository_info"
example:
- name: "my-app"
- namespace: "myorganization"
- repository_type: "image"
+ name: my-app
+ namespace: myorganization
+ repository_type: image
status: 1
- status_description: "Active"
- description: "A sample application repository"
+ status_description: Active
+ description: A sample application repository
is_private: false
is_automated: false
star_count: 0
@@ -1435,9 +1523,9 @@ paths:
last_updated: "2025-01-20T10:30:00Z"
date_registered: "2025-01-20T10:30:00Z"
collaborator_count: 0
- hub_user: "myorganization"
+ hub_user: myorganization
has_starred: false
- full_description: "This is a comprehensive description of my application repository that contains additional details about the project."
+ full_description: This is a comprehensive description of my application repository that contains additional details about the project.
media_types: []
content_types: []
categories: []
@@ -1446,15 +1534,20 @@ paths:
rules: []
storage_size: null
source: null
- 400:
+ user: myorganization
+ permissions:
+ read: true
+ write: true
+ admin: true
+ "400":
$ref: "#/components/responses/bad_request"
- 401:
+ "401":
$ref: "#/components/responses/unauthorized"
- 403:
+ "403":
$ref: "#/components/responses/forbidden"
- 404:
+ "404":
$ref: "#/components/responses/not_found"
- 500:
+ "500":
$ref: "#/components/responses/internal_error"
/v2/namespaces/{namespace}/repositories/{repository}:
parameters:
@@ -1463,30 +1556,30 @@ paths:
get:
operationId: GetRepository
summary: Get repository in a namespace
- description: |
+ description: |+
Returns a repository within the specified namespace (organization or user).
Public repositories are accessible to everyone, while private repositories require appropriate authentication and permissions.
-
+
tags:
- repositories
security:
- bearerAuth: []
- - {} # Allow anonymous access for public repositories
+ - {}
responses:
- 200:
+ "200":
content:
application/json:
schema:
$ref: "#/components/schemas/repository_info"
example:
- name: "my-app"
- namespace: "myorganization"
- repository_type: "image"
+ name: my-app
+ namespace: myorganization
+ repository_type: image
status: 1
- status_description: "Active"
- description: "A sample application repository"
+ status_description: Active
+ description: A sample application repository
is_private: false
is_automated: false
star_count: 0
@@ -1494,9 +1587,9 @@ paths:
last_updated: "2025-01-20T10:30:00Z"
date_registered: "2025-01-20T10:30:00Z"
collaborator_count: 0
- hub_user: "myorganization"
+ hub_user: myorganization
has_starred: false
- full_description: "This is a comprehensive description of my application repository that contains additional details about the project."
+ full_description: This is a comprehensive description of my application repository that contains additional details about the project.
media_types: []
content_types: []
categories: []
@@ -1505,66 +1598,42 @@ paths:
rules: []
storage_size: null
source: null
- 401:
+ user: myorganization
+ permissions:
+ read: true
+ write: true
+ admin: true
+ "401":
$ref: "#/components/responses/unauthorized"
- 403:
+ "403":
$ref: "#/components/responses/forbidden"
- 404:
+ "404":
$ref: "#/components/responses/not_found"
- 500:
+ "500":
$ref: "#/components/responses/internal_error"
head:
operationId: CheckRepository
summary: Check repository in a namespace
- description: |
+ description: |+
Check a repository within the specified namespace (organization or user).
Public repositories are accessible to everyone, while private repositories require appropriate authentication and permissions.
-
+
tags:
- repositories
security:
- bearerAuth: []
- - {} # Allow anonymous access for public repositories
+ - {}
responses:
- 200:
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/repository_info"
- example:
- name: "my-app"
- namespace: "myorganization"
- repository_type: "image"
- status: 1
- status_description: "Active"
- description: "A sample application repository"
- is_private: false
- is_automated: false
- star_count: 0
- pull_count: 0
- last_updated: "2025-01-20T10:30:00Z"
- date_registered: "2025-01-20T10:30:00Z"
- collaborator_count: 0
- hub_user: "myorganization"
- has_starred: false
- full_description: "This is a comprehensive description of my application repository that contains additional details about the project."
- media_types: []
- content_types: []
- categories: []
- immutable_tags_settings:
- enabled: false
- rules: []
- storage_size: null
- source: null
- 401:
+ "200": {}
+ "401":
$ref: "#/components/responses/unauthorized"
- 403:
+ "403":
$ref: "#/components/responses/forbidden"
- 404:
+ "404":
$ref: "#/components/responses/not_found"
- 500:
+ "500":
$ref: "#/components/responses/internal_error"
/v2/orgs/{org_name}/members:
parameters:
@@ -1577,7 +1646,7 @@ paths:
- $ref: "#/components/parameters/role"
get:
summary: List org members
- description: |
+ description: |+
Returns a list of members for an organization.
_The following fields are only visible to orgs with insights enabled._
@@ -1588,7 +1657,7 @@ paths:
To make visible, please see [View Insights for organization users](https://docs.docker.com/admin/insights/#view-insights-for-organization-users).
-
+
tags:
- orgs
security:
@@ -1599,9 +1668,7 @@ paths:
content:
application/json:
schema:
- type: array
- items:
- $ref: "#/components/schemas/org_member_paginated"
+ $ref: "#/components/schemas/org_member_paginated"
"400":
$ref: "#/components/responses/bad_request"
"401":
@@ -1610,15 +1677,16 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: getV2OrgsByOrgNameMembers
/v2/orgs/{org_name}/members/export:
parameters:
- $ref: "#/components/parameters/org_name"
get:
summary: Export org members CSV
- description: |
+ description: |+
Export members of an organization as a CSV
-
+
tags:
- orgs
security:
@@ -1629,46 +1697,11 @@ paths:
content:
text/csv:
schema:
- type: array
- items:
- type: object
- required:
- - Name
- - Username
- - Email
- - Type
- - Role
- - Date Joined
- properties:
- Name:
- type: string
- description: First and last name of the member
- Username:
- type: string
- description: Username of the member
- Email:
- type: string
- description: Email address of the member
- Type:
- type: string
- description: Type of the member
- enum:
- - Invitee
- - User
- Permission:
- type: string
- description: Permission of the member
- enum:
- - Owner
- - Member
- Teams:
- type: string
- description: Comma-separated list of teams the member is part of
- example: team-1, team-2
- Date Joined:
- type: string
- description: Date the member joined the organization
- example: 2020-01-01 15:00:51.193355 +0000 UTC
+ type: string
+ description: "CSV columns: Name, Username, Email, Type, Role, Teams, and Date Joined. The Role column contains the assigned role label. When administrative insights are available, the export also includes Last Logged-in Date, Docker Desktop Version, Last Seen Date, and Opted Out Analytics."
+ example: |
+ Name,Username,Email,Type,Role,Teams,Date Joined
+ Jon Snow,dockeruser,example@docker.com,User,Owner,owners,2021-01-05 21:06:53.5064 +0000 UTC
headers:
Content-Disposition:
schema:
@@ -1682,6 +1715,7 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: getV2OrgsByOrgNameMembersExport
/v2/orgs/{org_name}/members/{username}:
x-audience: public
parameters:
@@ -1689,11 +1723,11 @@ paths:
- $ref: "#/components/parameters/username"
put:
summary: Update org member (role)
- description: |
+ description: |+
Updates the role of a member in the organization.
***Only users in the "owners" group of the organization can use this endpoint.***
-
+
tags:
- orgs
security:
@@ -1703,6 +1737,8 @@ paths:
content:
application/json:
schema:
+ examples:
+ - role: owner
required:
- role
properties:
@@ -1729,12 +1765,13 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: putV2OrgsByOrgNameMembersByUsername
delete:
summary: Remove member from org
- description: |
+ description: |+
Removes the member from the org, ie. all groups in the org, unless they're the last owner
-
+
tags:
- orgs
security:
@@ -1750,16 +1787,17 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: deleteV2OrgsByOrgNameMembersByUsername
/v2/orgs/{org_name}/invites:
x-audience: public
parameters:
- $ref: "#/components/parameters/org_name"
get:
summary: List org invites
- description: |
+ description: |+
Return all pending invites for a given org, only team owners can call this endpoint
-
+
tags:
- invites
security:
@@ -1770,6 +1808,14 @@ paths:
content:
application/json:
schema:
+ examples:
+ - data:
+ - id: e36eca69-4cc8-4f17-9845-ae8c2b832691
+ inviter_username: moby
+ invitee: invitee@docker.com
+ org: docker
+ team: owners
+ created_at: "2021-10-28T18:30:19.520861Z"
type: object
properties:
data:
@@ -1782,14 +1828,14 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: getV2OrgsByOrgNameInvites
/v2/orgs/{org_name}/groups:
x-audience: public
parameters:
- $ref: "#/components/parameters/org_name"
get:
summary: Get groups of an organization
- description: |
-
+ description: Get groups of an organization.
tags:
- groups
security:
@@ -1818,27 +1864,44 @@ paths:
type: number
example: 1
next:
- type: string
+ type:
+ - string
+ - "null"
example: null
previous:
- type: string
+ type:
+ - string
+ - "null"
example: null
results:
type: array
items:
$ref: "#/components/schemas/org_group"
+ type: object
+ examples:
+ - count: 1
+ next: null
+ previous: null
+ results:
+ - id: 10
+ uuid: e36eca69-4cc8-4f17-9845-ae8c2b832691
+ name: developers
+ description: Development team
+ member_count: 1
+ role: member
"401":
$ref: "#/components/responses/unauthorized"
"403":
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: getV2OrgsByOrgNameGroups
post:
summary: Create a new group
- description: |
+ description: |+
Create a new group within an organization.
-
+
tags:
- groups
security:
@@ -1847,6 +1910,9 @@ paths:
content:
application/json:
schema:
+ examples:
+ - name: developers
+ description: Development team
required:
- name
properties:
@@ -1867,6 +1933,7 @@ paths:
$ref: "#/components/responses/unauthorized"
"403":
$ref: "#/components/responses/forbidden"
+ operationId: postV2OrgsByOrgNameGroups
/v2/orgs/{org_name}/groups/{group_name}:
x-audience: public
parameters:
@@ -1874,8 +1941,7 @@ paths:
- $ref: "#/components/parameters/group_name"
get:
summary: Get a group of an organization
- description: |
-
+ description: Get a group of an organization.
tags:
- groups
security:
@@ -1893,10 +1959,10 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: getV2OrgsByOrgNameGroupsByGroupName
put:
summary: Update the details for an organization group
- description: |
-
+ description: Update the details for an organization group.
tags:
- groups
security:
@@ -1905,6 +1971,9 @@ paths:
content:
application/json:
schema:
+ examples:
+ - name: developers
+ description: Development team
required:
- name
properties:
@@ -1925,10 +1994,10 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: putV2OrgsByOrgNameGroupsByGroupName
patch:
summary: Update some details for an organization group
- description: |
-
+ description: Update some details for an organization group.
tags:
- groups
security:
@@ -1937,6 +2006,9 @@ paths:
content:
application/json:
schema:
+ examples:
+ - description: Development team
+ role: member
properties:
name:
type: string
@@ -1962,10 +2034,10 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: patchV2OrgsByOrgNameGroupsByGroupName
delete:
summary: Delete an organization group
- description: |
-
+ description: Delete an organization group.
tags:
- groups
security:
@@ -1979,6 +2051,7 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: deleteV2OrgsByOrgNameGroupsByGroupName
/v2/orgs/{org_name}/groups/{group_name}/members:
x-audience: public
get:
@@ -1995,11 +2068,11 @@ paths:
type: string
description: Search members by username, full_name or email.
summary: List members of a group
- description: |
+ description: |+
List the members (users) that are in a group.
If user is owner of the org or has otherwise elevated permissions, they can search by email and the result will also contain emails.
-
+
tags:
- groups
responses:
@@ -2013,28 +2086,44 @@ paths:
type: number
example: 1
next:
- type: string
+ type:
+ - string
+ - "null"
example: null
previous:
- type: string
+ type:
+ - string
+ - "null"
example: null
results:
type: array
items:
$ref: "#/components/schemas/group_member"
+ type: object
+ examples:
+ - count: 1
+ next: null
+ previous: null
+ results:
+ - id: 0ab70deb065a43fcacd55d48caa945d8
+ username: dockeruser
+ full_name: Jon Snow
+ type: User
+ date_joined: "2021-01-05T21:06:53.506400Z"
+ email: example@docker.com
"401":
$ref: "#/components/responses/unauthorized"
"403":
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: getV2OrgsByOrgNameGroupsByGroupNameMembers
post:
parameters:
- $ref: "#/components/parameters/org_name"
- $ref: "#/components/parameters/group_name"
summary: Add a member to a group
- description: |
-
+ description: Add a member to a group.
tags:
- groups
security:
@@ -2052,6 +2141,7 @@ paths:
$ref: "#/components/responses/not_found"
"500":
$ref: "#/components/responses/internal_error"
+ operationId: postV2OrgsByOrgNameGroupsByGroupNameMembers
/v2/orgs/{org_name}/groups/{group_name}/members/{username}:
x-audience: public
parameters:
@@ -2060,8 +2150,7 @@ paths:
- $ref: "#/components/parameters/username"
delete:
summary: Remove a user from a group
- description: |
-
+ description: Remove a user from a group.
tags:
- groups
security:
@@ -2075,20 +2164,22 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: deleteV2OrgsByOrgNameGroupsByGroupNameMembersByUsername
/v2/invites/{id}:
x-audience: public
parameters:
- - in: path
+ - description: ID of the invitation.
+ in: path
name: id
required: true
schema:
type: string
delete:
summary: Cancel an invite
- description: |
+ description: |+
Mark the invite as cancelled so it doesn't show up on the list of pending invites
-
+
tags:
- invites
security:
@@ -2102,20 +2193,22 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: deleteV2InvitesById
/v2/invites/{id}/resend:
x-audience: public
parameters:
- - in: path
+ - description: ID of the invitation.
+ in: path
name: id
schema:
type: string
required: true
patch:
summary: Resend an invite
- description: |
+ description: |+
Resend a pending invite to the user, any org owner can resend an invite
-
+
tags:
- invites
security:
@@ -2129,16 +2222,17 @@ paths:
$ref: "#/components/responses/forbidden"
"404":
$ref: "#/components/responses/not_found"
+ operationId: patchV2InvitesByIdResend
/v2/invites/bulk:
x-audience: public
parameters:
- $ref: "#/components/parameters/bulk_invite"
post:
summary: Bulk create invites
- description: |
+ description: |+
Create multiple invites by emails or DockerIDs. Only a team owner can create invites.
-
+
tags:
- invites
requestBody:
@@ -2151,14 +2245,12 @@ paths:
content:
application/json:
schema:
- type: object
- properties:
- invitees:
- $ref: "#/components/schemas/bulk_invite"
+ $ref: "#/components/schemas/bulk_invite"
"400":
$ref: "#/components/responses/bad_request"
"409":
$ref: "#/components/responses/conflict"
+ operationId: postV2InvitesBulk
/v2/scim/2.0/ServiceProviderConfig:
x-audience: public
get:
@@ -2168,7 +2260,7 @@ paths:
tags:
- scim
security:
- - bearerSCIMAuth: []
+ - scimToken: []
responses:
"200":
$ref: "#/components/responses/scim_get_service_provider_config_resp"
@@ -2176,6 +2268,7 @@ paths:
$ref: "#/components/responses/scim_unauthorized"
"500":
$ref: "#/components/responses/scim_error"
+ operationId: getV2Scim20ServiceProviderConfig
/v2/scim/2.0/ResourceTypes:
x-audience: public
get:
@@ -2185,7 +2278,7 @@ paths:
tags:
- scim
security:
- - bearerSCIMAuth: []
+ - scimToken: []
responses:
"200":
$ref: "#/components/responses/scim_get_resource_types_resp"
@@ -2193,6 +2286,7 @@ paths:
$ref: "#/components/responses/scim_unauthorized"
"500":
$ref: "#/components/responses/scim_error"
+ operationId: getV2Scim20ResourceTypes
/v2/scim/2.0/ResourceTypes/{name}:
x-audience: public
get:
@@ -2202,14 +2296,15 @@ paths:
tags:
- scim
parameters:
- - name: name
+ - description: Name of the SCIM resource type.
+ name: name
in: path
schema:
type: string
example: User
required: true
security:
- - bearerSCIMAuth: []
+ - scimToken: []
responses:
"200":
$ref: "#/components/responses/scim_get_resource_type_resp"
@@ -2219,6 +2314,7 @@ paths:
$ref: "#/components/responses/scim_not_found"
"500":
$ref: "#/components/responses/scim_error"
+ operationId: getV2Scim20ResourceTypesByName
/v2/scim/2.0/Schemas:
x-audience: public
get:
@@ -2228,7 +2324,7 @@ paths:
tags:
- scim
security:
- - bearerSCIMAuth: []
+ - scimToken: []
responses:
"200":
$ref: "#/components/responses/scim_get_schemas_resp"
@@ -2236,6 +2332,7 @@ paths:
$ref: "#/components/responses/scim_unauthorized"
"500":
$ref: "#/components/responses/scim_error"
+ operationId: getV2Scim20Schemas
/v2/scim/2.0/Schemas/{id}:
x-audience: public
get:
@@ -2245,14 +2342,15 @@ paths:
tags:
- scim
parameters:
- - name: id
+ - description: URN identifying the SCIM schema.
+ name: id
in: path
schema:
type: string
example: urn:ietf:params:scim:schemas:core:2.0:User
required: true
security:
- - bearerSCIMAuth: []
+ - scimToken: []
responses:
"200":
$ref: "#/components/responses/scim_get_schema_resp"
@@ -2262,6 +2360,7 @@ paths:
$ref: "#/components/responses/scim_not_found"
"500":
$ref: "#/components/responses/scim_error"
+ operationId: getV2Scim20SchemasById
/v2/scim/2.0/Users:
x-audience: public
get:
@@ -2292,14 +2391,14 @@ paths:
tags:
- scim
security:
- - bearerSCIMAuth: []
+ - scimToken: []
parameters:
- name: startIndex
in: query
schema:
type: integer
minimum: 1
- description: ""
+ description: "One-based index of the first result to return."
example: 1
- name: count
in: query
@@ -2307,16 +2406,17 @@ paths:
type: integer
minimum: 1
maximum: 200
- description: ""
+ description: "Maximum number of results to return."
example: 10
- name: filter
in: query
schema:
type: string
- description: ""
+ description: "SCIM filter expression used to select users."
example: userName eq "jon.snow@docker.com"
- $ref: "#/components/parameters/scim_attributes"
- - name: sortOrder
+ - description: Sort direction applied to the attribute specified by sortBy.
+ name: sortOrder
in: query
schema:
type: string
@@ -2342,6 +2442,7 @@ paths:
$ref: "#/components/responses/scim_not_found"
"500":
$ref: "#/components/responses/scim_error"
+ operationId: getV2Scim20Users
post:
summary: Create user
description: |
@@ -2349,7 +2450,7 @@ paths:
tags:
- scim
security:
- - bearerSCIMAuth: []
+ - scimToken: []
requestBody:
$ref: "#/components/requestBodies/scim_create_user_request"
responses:
@@ -2367,6 +2468,7 @@ paths:
$ref: "#/components/responses/scim_conflict"
"500":
$ref: "#/components/responses/scim_error"
+ operationId: postV2Scim20Users
/v2/scim/2.0/Users/{id}:
x-audience: public
parameters:
@@ -2378,7 +2480,7 @@ paths:
tags:
- scim
security:
- - bearerSCIMAuth: []
+ - scimToken: []
responses:
"200":
$ref: "#/components/responses/scim_get_user_resp"
@@ -2392,6 +2494,7 @@ paths:
$ref: "#/components/responses/scim_not_found"
"500":
$ref: "#/components/responses/scim_error"
+ operationId: getV2Scim20UsersById
put:
summary: Update a user
description: |
@@ -2399,7 +2502,7 @@ paths:
tags:
- scim
security:
- - bearerSCIMAuth: []
+ - scimToken: []
requestBody:
$ref: "#/components/requestBodies/scim_update_user_request"
responses:
@@ -2417,6 +2520,7 @@ paths:
$ref: "#/components/responses/scim_conflict"
"500":
$ref: "#/components/responses/scim_error"
+ operationId: putV2Scim20UsersById
components:
responses:
BadRequest:
@@ -2424,25 +2528,49 @@ components:
content:
application/json:
schema:
- $ref: "#/components/schemas/ValueError"
+ $ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: bad request
+ errinfo: null
Unauthorized:
description: Unauthorized
content:
application/json:
schema:
- $ref: "#/components/schemas/Error"
+ $ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: unauthorized
+ errinfo: null
Forbidden:
description: Forbidden
content:
application/json:
schema:
- $ref: "#/components/schemas/Error"
+ $ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: permission denied
+ errinfo: null
NotFound:
description: Not Found
content:
application/json:
schema:
- $ref: "#/components/schemas/Error"
+ $ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: not found
+ errinfo: null
list_tags:
description: list repository tags
content:
@@ -2461,40 +2589,83 @@ components:
application/json:
schema:
$ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: bad request
+ errinfo: null
unauthorized:
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: unauthorized
+ errinfo: null
forbidden:
description: Forbidden
content:
application/json:
schema:
$ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: permission denied
+ errinfo: null
not_found:
description: Not Found
content:
application/json:
schema:
$ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: not found
+ errinfo: null
conflict:
description: Conflict
content:
application/json:
schema:
$ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: conflict
+ errinfo: null
internal_error:
description: Internal
content:
application/json:
schema:
$ref: "#/components/schemas/error"
+ examples:
+ error:
+ summary: Example error response
+ value:
+ message: internal error
+ errinfo: null
scim_bad_request:
description: Bad Request
content:
application/scim+json:
+ examples:
+ error:
+ summary: Example error envelope
+ value:
+ schemas:
+ - urn:ietf:params:scim:api:messages:2.0:Error
+ status: "400"
schema:
allOf:
- $ref: "#/components/schemas/scim_error"
@@ -2508,6 +2679,13 @@ components:
description: Unauthorized
content:
application/scim+json:
+ examples:
+ error:
+ summary: Example error envelope
+ value:
+ schemas:
+ - urn:ietf:params:scim:api:messages:2.0:Error
+ status: "401"
schema:
allOf:
- $ref: "#/components/schemas/scim_error"
@@ -2518,6 +2696,13 @@ components:
description: Forbidden
content:
application/scim+json:
+ examples:
+ error:
+ summary: Example error envelope
+ value:
+ schemas:
+ - urn:ietf:params:scim:api:messages:2.0:Error
+ status: "403"
schema:
allOf:
- $ref: "#/components/schemas/scim_error"
@@ -2528,6 +2713,13 @@ components:
description: Not Found
content:
application/scim+json:
+ examples:
+ error:
+ summary: Example error envelope
+ value:
+ schemas:
+ - urn:ietf:params:scim:api:messages:2.0:Error
+ status: "404"
schema:
allOf:
- $ref: "#/components/schemas/scim_error"
@@ -2538,6 +2730,13 @@ components:
description: Conflict
content:
application/scim+json:
+ examples:
+ error:
+ summary: Example error envelope
+ value:
+ schemas:
+ - urn:ietf:params:scim:api:messages:2.0:Error
+ status: "409"
schema:
allOf:
- $ref: "#/components/schemas/scim_error"
@@ -2548,6 +2747,13 @@ components:
description: Internal Error
content:
application/scim+json:
+ examples:
+ error:
+ summary: Example error envelope
+ value:
+ schemas:
+ - urn:ietf:params:scim:api:messages:2.0:Error
+ status: "500"
schema:
allOf:
- $ref: "#/components/schemas/scim_error"
@@ -2575,10 +2781,22 @@ components:
totalResults:
type: integer
example: 1
- resources:
+ Resources:
type: array
items:
$ref: "#/components/schemas/scim_resource_type"
+ examples:
+ - schemas:
+ - urn:ietf:params:scim:api:messages:2.0:ListResponse
+ totalResults: 1
+ Resources:
+ - schemas:
+ - urn:ietf:params:scim:schemas:core:2.0:ResourceType
+ id: User
+ name: User
+ description: User
+ endpoint: /Users
+ schema: urn:ietf:params:scim:schemas:core:2.0:User
scim_get_resource_type_resp:
description: ""
content:
@@ -2600,10 +2818,21 @@ components:
totalResults:
type: integer
example: 1
- resources:
+ Resources:
type: array
items:
$ref: "#/components/schemas/scim_schema"
+ examples:
+ - schemas:
+ - urn:ietf:params:scim:api:messages:2.0:ListResponse
+ totalResults: 1
+ Resources:
+ - schemas:
+ - urn:ietf:params:scim:schemas:core:2.0:Schema
+ id: urn:ietf:params:scim:schemas:core:2.0:User
+ name: User
+ description: User Account
+ attributes: []
scim_get_schema_resp:
description: ""
content:
@@ -2632,10 +2861,38 @@ components:
itemsPerPage:
type: integer
example: 10
- resources:
+ Resources:
type: array
items:
$ref: "#/components/schemas/scim_user"
+ examples:
+ - schemas:
+ - urn:ietf:params:scim:api:messages:2.0:ListResponse
+ totalResults: 1
+ Resources:
+ - schemas:
+ - urn:ietf:params:scim:schemas:core:2.0:User
+ id: d80f7c79-7730-49d8-9a41-7c42fb622d9c
+ userName: jon.snow@docker.com
+ name:
+ givenName: Jon
+ familyName: Snow
+ displayName: jonsnow
+ active: true
+ emails:
+ - value: jon.snow@docker.com
+ display: jon.snow@docker.com
+ primary: true
+ groups:
+ - value: nightswatch
+ display: nightswatch
+ meta:
+ resourceType: User
+ location: https://hub.docker.com/v2/scim/2.0/Users/d80f7c79-7730-49d8-9a41-7c42fb622d9c
+ created: "2022-05-20T00:54:18Z"
+ lastModified: "2022-05-20T00:54:18Z"
+ startIndex: 1
+ itemsPerPage: 1
scim_create_user_resp:
description: ""
content:
@@ -2679,11 +2936,16 @@ components:
type: string
description: List of immutable tag rules
example:
- - "v.*"
- - ".*-RELEASE"
+ - v.*
+ - .*-RELEASE
required:
- immutable_tags
- immutable_tags_rules
+ examples:
+ - immutable_tags: true
+ immutable_tags_rules:
+ - v.*
+ - .*-RELEASE
repo_creation_request:
type: object
required:
@@ -2698,26 +2960,26 @@ components:
Letters must be lowercase.
minLength: 2
maxLength: 255
- pattern: "^[a-z0-9]+(?:[._-][a-z0-9]+)*$"
- example: "my-app"
+ pattern: ^[a-z0-9]+(?:[._-][a-z0-9]+)*$
+ example: my-app
namespace:
type: string
description: The namespace where the repository will be created
- example: "myorganization"
+ example: myorganization
description:
type: string
description: Short description of the repository
maxLength: 100
- example: "A sample application repository"
+ example: A sample application repository
full_description:
type: string
description: Detailed description of the repository
maxLength: 25000
- example: "This is a comprehensive description of my application repository that contains additional details about the project, its purpose, usage instructions, and other relevant information."
+ example: This is a comprehensive description of my application repository that contains additional details about the project, its purpose, usage instructions, and other relevant information.
registry:
type: string
description: The registry where the repository will be hosted
- example: "docker.io"
+ example: docker.io
is_private:
type: boolean
description: Whether the repository should be private
@@ -2741,20 +3003,26 @@ components:
- read: Can view and pull from the repository
- write: Can view, pull, and push to the repository
- admin: Can view, pull, push, and manage repository settings
- enum: ["read", "write", "admin"]
- example: "write"
+ enum:
+ - read
+ - write
+ - admin
+ example: write
RepositoryGroup:
type: object
properties:
group_name:
type: string
description: The name of the group
- example: "developers"
+ example: developers
permission:
type: string
description: The permission level granted to the group
- enum: ["read", "write", "admin"]
- example: "write"
+ enum:
+ - read
+ - write
+ - admin
+ example: write
group_id:
type: integer
format: int64
@@ -2773,8 +3041,9 @@ components:
type: string
description: Repository namespace
repository_type:
- type: string
- nullable: true
+ type:
+ - string
+ - "null"
description: Type of the repository
status:
type: integer
@@ -2805,10 +3074,11 @@ components:
example: "2021-01-05T21:06:53.506400Z"
description: ISO 8601 timestamp of when repository was last updated
last_modified:
- type: string
+ type:
+ - string
+ - "null"
format: date-time
example: "2021-01-05T21:06:53.506400Z"
- nullable: true
description: ISO 8601 timestamp of when repository was last modified
date_registered:
type: string
@@ -2820,27 +3090,31 @@ components:
format: int64
description: Number of collaborators
affiliation:
- type: string
- nullable: true
+ type:
+ - string
+ - "null"
description: Repository affiliation
hub_user:
- type: string
- nullable: true
+ type:
+ - string
+ - "null"
description: Hub user information
has_starred:
type: boolean
description: Whether the current user has starred this repository
full_description:
- type: string
- nullable: true
+ type:
+ - string
+ - "null"
description: Full description of the repository
permissions:
$ref: "#/components/schemas/repo_permissions"
media_types:
type: array
items:
- type: string
- nullable: true
+ type:
+ - string
+ - "null"
description: Supported media types
content_types:
type: array
@@ -2855,13 +3129,15 @@ components:
immutable_tags_settings:
$ref: "#/components/schemas/immutable_tags_settings"
storage_size:
- type: integer
+ type:
+ - integer
+ - "null"
format: int64
- nullable: true
description: Storage size in bytes
source:
- type: string
- nullable: true
+ type:
+ - string
+ - "null"
description: Source of the repository, where it was created from
required:
- user
@@ -2883,6 +3159,36 @@ components:
- content_types
- categories
- immutable_tags_settings
+ examples:
+ - name: my-app
+ namespace: myorganization
+ repository_type: image
+ status: 1
+ status_description: Active
+ description: A sample application repository
+ is_private: false
+ is_automated: false
+ star_count: 0
+ pull_count: 0
+ last_updated: "2025-01-20T10:30:00Z"
+ date_registered: "2025-01-20T10:30:00Z"
+ collaborator_count: 0
+ hub_user: myorganization
+ has_starred: false
+ full_description: This is a comprehensive description of my application repository that contains additional details about the project.
+ media_types: []
+ content_types: []
+ categories: []
+ immutable_tags_settings:
+ enabled: false
+ rules: []
+ storage_size: null
+ source: null
+ user: myorganization
+ permissions:
+ read: true
+ write: true
+ admin: true
repo_permissions:
type: object
properties:
@@ -2918,12 +3224,19 @@ components:
properties:
regex:
type: string
- pattern: '^[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*(\\/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*$'
- description: 'Immutable tags rule regex pattern. Must match format: [a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*(\\/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*'
- example: "v.*"
+ pattern: ^[^,]*$
+ description: Regular expression using Go regular expression syntax. Commas are not permitted.
+ example: v.*
required:
- regex
+ examples:
+ - regex: v.*
immutable_tags_verify_response:
+ examples:
+ - tags:
+ - v1.0.0
+ - v2.1.3
+ - latest
type: object
properties:
tags:
@@ -2932,9 +3245,9 @@ components:
type: string
description: List of tags that match the provided regex pattern
example:
- - "v1.0.0"
- - "v2.1.3"
- - "latest"
+ - v1.0.0
+ - v2.1.3
+ - latest
required:
- tags
repository_list_entry:
@@ -2943,20 +3256,21 @@ components:
name:
type: string
description: Name of the repository
- example: "hello-world"
+ example: hello-world
namespace:
type: string
description: Namespace (organization or username) that owns the repository
- example: "docker"
+ example: docker
repository_type:
- type: string
+ type:
+ - string
+ - "null"
description: Type of repository
enum:
- image
- plugin
- null
- example: "image"
- nullable: true
+ example: image
status:
type: integer
description: Repository status code
@@ -2967,12 +3281,13 @@ components:
enum:
- active
- inactive
- example: "active"
+ example: active
description:
- type: string
+ type:
+ - string
+ - "null"
description: Repository description
- nullable: true
- example: "Hello World! (an example of minimal Dockerization)"
+ example: Hello World! (an example of minimal Dockerization)
is_private:
type: boolean
description: Whether the repository is private
@@ -2988,22 +3303,24 @@ components:
minimum: 0
example: 50000000
last_updated:
- type: string
+ type:
+ - string
+ - "null"
format: date-time
description: ISO 8601 timestamp of when the repository was last updated
- example: "2023-12-01T10:30:00Z"
- nullable: true
+ example: 2023-12-01T10:30:00Z
last_modified:
- type: string
+ type:
+ - string
+ - "null"
format: date-time
description: ISO 8601 timestamp of when the repository was last modified
- example: "2023-12-01T10:30:00Z"
- nullable: true
+ example: 2023-12-01T10:30:00Z
date_registered:
type: string
format: date-time
description: ISO 8601 timestamp of when the repository was created
- example: "2013-06-19T19:07:54Z"
+ example: 2013-06-19T19:07:54Z
affiliation:
type: string
description: User's affiliation with the repository (empty string if no affiliation)
@@ -3014,14 +3331,14 @@ components:
items:
type: string
example:
- - "application/vnd.docker.plugin.v1+json"
+ - application/vnd.docker.plugin.v1+json
content_types:
type: array
description: Content types supported by this repository
items:
type: string
example:
- - "plugin"
+ - plugin
categories:
type: array
description: Categories associated with this repository
@@ -3042,14 +3359,14 @@ components:
name:
type: string
description: Human-readable name of the category
- example: "Databases"
+ example: Databases
minLength: 1
slug:
type: string
description: URL-friendly identifier for the category
- example: "databases"
+ example: databases
minLength: 1
- pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
description: Repository category for classification and discovery
list_repositories_response:
allOf:
@@ -3061,6 +3378,9 @@ components:
items:
$ref: "#/components/schemas/repository_list_entry"
UsersLoginRequest:
+ examples:
+ - username: myusername
+ password: p@ssw0rd
description: User login details
type: object
required:
@@ -3077,6 +3397,8 @@ components:
type: string
example: p@ssw0rd
AuthCreateTokenResponse:
+ examples:
+ - access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
description: successful access token response
type: object
properties:
@@ -3085,6 +3407,8 @@ components:
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
PostUsersLoginSuccessResponse:
+ examples:
+ - token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
description: successful user login response
type: object
properties:
@@ -3094,8 +3418,9 @@ components:
This token can be used in the HTTP Authorization header as a JWT to authenticate with the Docker Hub APIs.
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- nullable: false
PostUsersLoginErrorResponse:
+ examples:
+ - detail: Incorrect authentication credentials
description: failed user login response or second factor required
type: object
required:
@@ -3105,14 +3430,17 @@ components:
description: Description of the error.
type: string
example: Incorrect authentication credentials
- nullable: false
login_2fa_token:
description: |
Short time lived token to be used on `/v2/users/2fa-login` to complete the authentication. This field is present only if 2FA is enabled.
- type: string
+ type:
+ - string
+ - "null"
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- nullable: true
Users2FALoginRequest:
+ examples:
+ - login_2fa_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
+ code: "123456"
description: Second factor user login details
type: object
required:
@@ -3127,8 +3455,10 @@ components:
description: |
The Time-based One-Time Password of the Docker Hub account to authenticate with.
type: string
- example: 123456
+ example: "123456"
PostUsers2FALoginErrorResponse:
+ examples:
+ - detail: Incorrect authentication credentials
description: failed second factor login response.
type: object
properties:
@@ -3136,27 +3466,6 @@ components:
description: Description of the error.
type: string
example: Incorrect authentication credentials
- nullable: false
- protobufAny:
- type: object
- properties:
- type_url:
- type: string
- value:
- type: string
- format: byte
- rpcStatus:
- type: object
- properties:
- code:
- type: integer
- format: int32
- message:
- type: string
- details:
- type: array
- items:
- $ref: "#/components/schemas/protobufAny"
AuditLogAction:
type: object
properties:
@@ -3220,23 +3529,6 @@ components:
action_description:
type: string
description: Audit log event.
- ValueError:
- type: object
- description: Used to error if input validation fails.
- properties:
- fields:
- type: object
- items:
- type: string
- text:
- type: string
- Error:
- type: object
- properties:
- detail:
- type: string
- message:
- type: string
accessToken:
type: object
properties:
@@ -3256,9 +3548,10 @@ components:
type: string
example: "2021-07-20T12:00:00.000000Z"
last_used:
- type: string
+ type:
+ - string
+ - "null"
example: null
- nullable: true
generated_by:
type: string
example: manual
@@ -3268,6 +3561,7 @@ components:
token:
type: string
example: a7a5ef25-8889-43a0-8cc7-f2a94268e861
+ description: Token value returned on creation. Later responses redact the value as ***.
token_label:
type: string
example: My read only token
@@ -3278,10 +3572,16 @@ components:
items:
type: string
expires_at:
- type: string
+ type:
+ - string
+ - "null"
format: date-time
example: "2021-10-28T18:30:19.520861Z"
createAccessTokenRequest:
+ examples:
+ - token_label: My read only token
+ scopes:
+ - repo:read
type: object
required:
- token_label
@@ -3307,8 +3607,22 @@ components:
Optional expiration date for the token.
If omitted, the token will remain valid indefinitely.
format: date-time
- example: "2021-10-28T18:30:19.520861Z"
+ example: 2021-10-28T18:30:19.520861Z
createAccessTokensResponse:
+ examples:
+ - uuid: b30bbf97-506c-4ecd-aabc-842f3cb484fb
+ client_id: HUB
+ creator_ip: 127.0.0.1
+ creator_ua: some user agent
+ created_at: "2021-07-20T12:00:00.000000Z"
+ last_used: null
+ generated_by: manual
+ is_active: true
+ token: a7a5ef25-8889-43a0-8cc7-f2a94268e861
+ token_label: My read only token
+ scopes:
+ - repo:read
+ expires_at: "2021-10-28T18:30:19.520861Z"
$ref: "#/components/schemas/accessToken"
getAccessTokensResponse:
type: object
@@ -3317,10 +3631,14 @@ components:
type: number
example: 1
next:
- type: string
+ type:
+ - string
+ - "null"
example: null
previous:
- type: string
+ type:
+ - string
+ - "null"
example: null
active_count:
type: number
@@ -3334,8 +3652,30 @@ components:
properties:
token:
type: string
- example: ""
+ example: "***"
+ examples:
+ - count: 1
+ active_count: 1
+ next: null
+ previous: null
+ results:
+ - uuid: b30bbf97-506c-4ecd-aabc-842f3cb484fb
+ client_id: HUB
+ creator_ip: 127.0.0.1
+ creator_ua: some user agent
+ created_at: "2021-07-20T12:00:00.000000Z"
+ last_used: null
+ generated_by: manual
+ is_active: true
+ token: "***"
+ token_label: My read only token
+ scopes:
+ - repo:read
+ expires_at: "2021-10-28T18:30:19.520861Z"
patchAccessTokenRequest:
+ examples:
+ - token_label: My read only token
+ is_active: false
type: object
properties:
token_label:
@@ -3348,7 +3688,26 @@ components:
example: false
patchAccessTokenResponse:
$ref: "#/components/schemas/accessToken"
+ examples:
+ - uuid: b30bbf97-506c-4ecd-aabc-842f3cb484fb
+ client_id: HUB
+ creator_ip: 127.0.0.1
+ creator_ua: some user agent
+ created_at: "2021-07-20T12:00:00.000000Z"
+ last_used: null
+ generated_by: manual
+ is_active: true
+ token: "***"
+ token_label: My read only token
+ scopes:
+ - repo:read
+ expires_at: "2021-10-28T18:30:19.520861Z"
orgSettings:
+ examples:
+ - restricted_images:
+ enabled: true
+ allow_official_images: true
+ allow_verified_publishers: true
type: object
properties:
restricted_images:
@@ -3372,9 +3731,10 @@ components:
type: object
properties:
digest:
- type: string
+ type:
+ - string
+ - "null"
description: image layer digest
- nullable: true
size:
type: integer
description: size of the layer
@@ -3388,15 +3748,20 @@ components:
type: string
description: CPU architecture
features:
- type: string
+ type:
+ - string
+ - "null"
description: CPU features
variant:
- type: string
+ type:
+ - string
+ - "null"
description: CPU variant
digest:
- type: string
+ type:
+ - string
+ - "null"
description: image digest
- nullable: true
layers:
type: array
items:
@@ -3405,10 +3770,14 @@ components:
type: string
description: operating system
os_features:
- type: string
+ type:
+ - string
+ - "null"
description: OS features
os_version:
- type: string
+ type:
+ - string
+ - "null"
description: OS version
size:
type: integer
@@ -3420,15 +3789,17 @@ components:
- inactive
description: Status of the image
last_pulled:
- type: string
+ type:
+ - string
+ - "null"
example: "2021-01-05T21:06:53.506400Z"
description: datetime of last pull
- nullable: true
last_pushed:
- type: string
+ type:
+ - string
+ - "null"
example: "2021-01-05T21:06:53.506400Z"
description: datetime of last push
- nullable: true
tag:
type: object
properties:
@@ -3436,16 +3807,20 @@ components:
type: integer
description: tag ID
images:
- type: object
- $ref: "#/components/schemas/image"
+ type:
+ - array
+ - "null"
+ items:
+ $ref: "#/components/schemas/image"
creator:
type: integer
description: ID of the user that pushed the tag
last_updated:
- type: string
+ type:
+ - string
+ - "null"
example: "2021-01-05T21:06:53.506400Z"
description: datetime of last update
- nullable: true
last_updater:
type: integer
description: ID of the last user that updated the tag
@@ -3462,24 +3837,50 @@ components:
type: integer
description: compressed size (sum of all layers) of the tagged image
v2:
- type: string
- description: repository API version
- status:
- type: string
- enum:
- - active
- - inactive
- description: whether a tag has been pushed to or pulled in the past month
+ type: boolean
+ description: Whether the tag uses the Registry V2 format.
tag_last_pulled:
- type: string
+ type:
+ - string
+ - "null"
example: "2021-01-05T21:06:53.506400Z"
description: datetime of last pull
- nullable: true
tag_last_pushed:
- type: string
+ type:
+ - string
+ - "null"
example: "2021-01-05T21:06:53.506400Z"
description: datetime of last push
- nullable: true
+ tag_status:
+ type: string
+ enum:
+ - active
+ - inactive
+ description: whether a tag has been pushed to or pulled in the past month
+ examples:
+ - id: 12345
+ name: latest
+ last_updated: "2021-01-05T21:06:53.506400Z"
+ full_size: 123456
+ v2: true
+ images:
+ - architecture: amd64
+ features: null
+ variant: null
+ os: linux
+ os_features: null
+ os_version: null
+ size: 123456
+ status: active
+ last_pulled: null
+ last_pushed: "2021-01-05T21:06:53.506400Z"
+ creator: 1234
+ last_updater: 1234
+ last_updater_username: dockeruser
+ repository: 5678
+ tag_status: active
+ tag_last_pulled: null
+ tag_last_pushed: "2021-01-05T21:06:53.506400Z"
paginated_tags:
allOf:
- $ref: "#/components/schemas/page"
@@ -3489,6 +3890,34 @@ components:
type: array
items:
$ref: "#/components/schemas/tag"
+ examples:
+ - count: 1
+ next: null
+ previous: null
+ results:
+ - id: 12345
+ name: latest
+ last_updated: "2021-01-05T21:06:53.506400Z"
+ full_size: 123456
+ v2: true
+ images:
+ - architecture: amd64
+ features: null
+ variant: null
+ os: linux
+ os_features: null
+ os_version: null
+ size: 123456
+ status: active
+ last_pulled: null
+ last_pushed: "2021-01-05T21:06:53.506400Z"
+ creator: 1234
+ last_updater: 1234
+ last_updater_username: dockeruser
+ repository: 5678
+ tag_status: active
+ tag_last_pulled: null
+ tag_last_pushed: "2021-01-05T21:06:53.506400Z"
page:
type: object
properties:
@@ -3496,13 +3925,15 @@ components:
type: integer
description: total number of results available across all pages
next:
- type: string
+ type:
+ - string
+ - "null"
description: link to next page of results if any
- nullable: true
previous:
- type: string
+ type:
+ - string
+ - "null"
description: link to previous page of results if any
- nullable: true
scim_schema_attribute:
type: object
properties:
@@ -3570,7 +4001,7 @@ components:
example: owners
created_at:
type: string
- example: "2021-10-28T18:30:19.520861Z"
+ example: 2021-10-28T18:30:19.520861Z
bulk_invite:
type: object
properties:
@@ -3599,7 +4030,7 @@ components:
invitee: invitee@docker.com
org: docker
team: owners
- created_at: "2021-10-28T18:30:19.520861Z"
+ created_at: 2021-10-28T18:30:19.520861Z
- invitee: invitee2@docker.com
status: existing_org_member
- invitee: invitee3@docker.com
@@ -3608,13 +4039,16 @@ components:
type: object
properties:
errinfo:
- type: object
- items:
- type: string
+ type:
+ - object
+ - "null"
+ additionalProperties: true
+ description: Additional error context, when available.
detail:
type: string
message:
type: string
+ description: Error details vary by endpoint. The message describes the failure; errinfo can contain additional context.
scim_error:
type: object
properties:
@@ -3641,7 +4075,7 @@ components:
example: Docker Inc
date_joined:
type: string
- example: "2021-01-05T21:06:53.506400Z"
+ example: 2021-01-05T21:06:53.506400Z
full_name:
type: string
example: Jon Snow
@@ -3699,26 +4133,40 @@ components:
example: example@docker.com
deprecated: true
last_logged_in_at:
- type: string
+ type:
+ - string
+ - "null"
format: date-time
description: |
- Last time the user logged in. To access this field, you must have insights visible for your organization. See
+ Last time the user logged in. To access this field, you must have insights visible for your organization. See
[Insights](https://docs.docker.com/admin/insights/#view-insights-for-organization-users).
example: "2021-01-05T21:06:53.506400Z"
last_seen_at:
- type: string
+ type:
+ - string
+ - "null"
format: date-time
description: |
- Last time the user was seen. To access this field, you must have insights visible for your organization. See
+ Last time the user was seen. To access this field, you must have insights visible for your organization. See
[Insights](https://docs.docker.com/admin/insights/#view-insights-for-organization-users).
example: "2021-01-05T21:06:53.506400Z"
last_desktop_version:
type: string
description: |
- Last desktop version the user used. To access this field, you must have insights visible for your organization. See
+ Last desktop version the user used. To access this field, you must have insights visible for your organization. See
[Insights](https://docs.docker.com/admin/insights/#view-insights-for-organization-users).
example: 4.29.0
-
+ examples:
+ - id: 0ab70deb065a43fcacd55d48caa945d8
+ username: dockeruser
+ full_name: Jon Snow
+ type: User
+ date_joined: "2021-01-05T21:06:53.506400Z"
+ email: example@docker.com
+ role: Owner
+ groups:
+ - owners
+ is_guest: false
org_member_paginated:
type: object
properties:
@@ -3727,18 +4175,37 @@ components:
description: The total number of items that match with the search.
example: 120
previous:
- type: string
+ type:
+ - string
+ - "null"
description: The URL or link for the previous page of items.
- example: https://hub.docker.com/v2/some/resources/items?page=1&page_size=20
+ example: null
next:
- type: string
+ type:
+ - string
+ - "null"
description: The URL or link for the next page of items.
- example: https://hub.docker.com/v2/some/resources/items?page=3&page_size=20
+ example: null
results:
type: array
description: List of accounts.
items:
$ref: "#/components/schemas/org_member"
+ examples:
+ - count: 1
+ next: null
+ previous: null
+ results:
+ - id: 0ab70deb065a43fcacd55d48caa945d8
+ username: dockeruser
+ full_name: Jon Snow
+ type: User
+ date_joined: "2021-01-05T21:06:53.506400Z"
+ email: example@docker.com
+ role: Owner
+ groups:
+ - owners
+ is_guest: false
org_group:
type: object
properties:
@@ -3762,10 +4229,19 @@ components:
example: 10
description: Member count of the group
role:
- type: string
+ type:
+ - string
+ - "null"
description: |
Role assigned to the team. A core role (`owner`, `editor`, or `member`)
or the name of a custom role (not the label or UUID).
+ examples:
+ - id: 10
+ uuid: e36eca69-4cc8-4f17-9845-ae8c2b832691
+ name: developers
+ description: Development team
+ member_count: 1
+ role: member
group_member:
type: object
properties:
@@ -3779,7 +4255,7 @@ components:
date_joined:
type: string
format: date-time
- example: "2021-01-05T21:06:53.506400Z"
+ example: 2021-01-05T21:06:53.506400Z
full_name:
type: string
example: John Snow
@@ -3849,7 +4325,8 @@ components:
properties:
supported:
type: boolean
- example: false
+ example: true
+ type: object
bulk:
type: object
properties:
@@ -3868,7 +4345,7 @@ components:
example: true
maxResults:
type: integer
- example: 99999
+ example: 200
changePassword:
type: object
properties:
@@ -3888,21 +4365,53 @@ components:
type: boolean
example: false
authenticationSchemes:
- type: object
- properties:
- name:
- type: string
- example: OAuth 2.0 Bearer Token
- description:
- type: string
- example: The OAuth 2.0 Bearer Token Authentication scheme. OAuth enables clients to access protected resources by obtaining an access token, which is defined in RFC 6750 as "a string representing an access authorization issued to the client", rather than using the resource owner's credentials directly.
- specUri:
- type: string
- example: http://tools.ietf.org/html/rfc6750
- type:
- type: string
- example: oauthbearertoken
+ type: array
+ items:
+ type: object
+ properties:
+ name:
+ type: string
+ example: OAuth 2.0 Bearer Token
+ description:
+ type: string
+ example: The OAuth 2.0 Bearer Token Authentication scheme. OAuth enables clients to access protected resources by obtaining an access token, which is defined in RFC 6750 as "a string representing an access authorization issued to the client", rather than using the resource owner's credentials directly.
+ specUri:
+ type: string
+ example: http://tools.ietf.org/html/rfc6750
+ type:
+ type: string
+ example: oauthbearertoken
+ examples:
+ - schemas:
+ - urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig
+ patch:
+ supported: true
+ bulk:
+ supported: false
+ maxOperations: 0
+ maxPayloadSize: 0
+ filter:
+ supported: true
+ maxResults: 200
+ changePassword:
+ supported: false
+ sort:
+ supported: true
+ etag:
+ supported: false
+ authenticationSchemes:
+ - name: OAuth 2.0 Bearer Token
+ specUri: http://tools.ietf.org/html/rfc6750
+ type: oauthbearertoken
scim_resource_type:
+ examples:
+ - schemas:
+ - urn:ietf:params:scim:schemas:core:2.0:ResourceType
+ id: User
+ name: User
+ description: User
+ endpoint: /Users
+ schema: urn:ietf:params:scim:schemas:core:2.0:User
type: object
properties:
schemas:
@@ -3927,6 +4436,13 @@ components:
type: string
example: urn:ietf:params:scim:schemas:core:2.0:User
scim_schema:
+ examples:
+ - schemas:
+ - urn:ietf:params:scim:schemas:core:2.0:Schema
+ id: urn:ietf:params:scim:schemas:core:2.0:User
+ name: User
+ description: User Account
+ attributes: []
type: object
properties:
schemas:
@@ -3998,6 +4514,28 @@ components:
example: d80f7c79-7730-49d8-9a41-7c42fb622d9c
description: The unique identifier for the user. A v4 UUID.
scim_user:
+ examples:
+ - schemas:
+ - urn:ietf:params:scim:schemas:core:2.0:User
+ id: d80f7c79-7730-49d8-9a41-7c42fb622d9c
+ userName: jon.snow@docker.com
+ name:
+ givenName: Jon
+ familyName: Snow
+ displayName: jonsnow
+ active: true
+ emails:
+ - value: jon.snow@docker.com
+ display: jon.snow@docker.com
+ primary: true
+ groups:
+ - value: nightswatch
+ display: nightswatch
+ meta:
+ resourceType: User
+ location: https://hub.docker.com/v2/scim/2.0/Users/d80f7c79-7730-49d8-9a41-7c42fb622d9c
+ created: "2022-05-20T00:54:18Z"
+ lastModified: "2022-05-20T00:54:18Z"
type: object
properties:
schemas:
@@ -4034,41 +4572,43 @@ components:
type: string
format: date-time
description: The creation date for the user as a RFC3339 formatted string.
- example: "2022-05-20T00:54:18Z"
+ example: 2022-05-20T00:54:18Z
lastModified:
type: string
format: date-time
description: The date the user was last modified as a RFC3339 formatted string.
- example: "2022-05-20T00:54:18Z"
+ example: 2022-05-20T00:54:18Z
orgAccessToken:
type: object
properties:
id:
type: string
- example: "a7a5ef25-8889-43a0-8cc7-f2a94268e861"
+ example: a7a5ef25-8889-43a0-8cc7-f2a94268e861
label:
type: string
- example: "My organization token"
+ example: My organization token
created_by:
type: string
- example: "johndoe"
+ example: johndoe
is_active:
type: boolean
example: true
created_at:
type: string
format: date-time
- example: "2022-05-20T00:54:18Z"
+ example: 2022-05-20T00:54:18Z
expires_at:
- type: string
+ type:
+ - string
+ - "null"
format: date-time
- example: "2023-05-20T00:54:18Z"
- nullable: true
+ example: 2023-05-20T00:54:18Z
last_used_at:
- type: string
+ type:
+ - string
+ - "null"
format: date-time
- example: "2022-06-15T12:30:45Z"
- nullable: true
+ example: 2022-06-15T12:30:45Z
orgAccessTokenResource:
type: object
properties:
@@ -4077,12 +4617,11 @@ components:
enum:
- TYPE_REPO
- TYPE_ORG
- example: "TYPE_REPO"
+ example: TYPE_REPO
description: The type of resource
- required: true
path:
type: string
- example: "myorg/myrepo"
+ example: myorg/myrepo
description: |
The path of the resource. The format of this will change depending on the type of resource.
@@ -4090,14 +4629,16 @@ components:
- Must be an existing repository name (e.g., "myorg/myrepo")
- Can use glob patterns (e.g., "myorg/*" for all repositories in the organization)
- Use "*/*/public" to reference all public repositories
- required: true
scopes:
type: array
description: The scopes this token has access to
items:
type: string
- example: "scope-image-pull"
- required: true
+ example: scope-image-pull
+ required:
+ - type
+ - path
+ - scopes
getOrgAccessTokensResponse:
type: object
properties:
@@ -4105,16 +4646,45 @@ components:
type: number
example: 10
next:
- type: string
- example: https://hub.docker.com/v2/orgs/docker/access-tokens?page=2&page_size=10
+ type:
+ - string
+ - "null"
+ example: null
previous:
- type: string
- example: https://hub.docker.com/v2/orgs/docker/access-tokens?page=1&page_size=10
+ type:
+ - string
+ - "null"
+ example: null
results:
type: array
items:
$ref: "#/components/schemas/orgAccessToken"
+ examples:
+ - total: 1
+ next: null
+ previous: null
+ results:
+ - id: a7a5ef25-8889-43a0-8cc7-f2a94268e861
+ label: My organization token
+ created_by: johndoe
+ is_active: true
+ created_at: "2022-05-20T00:54:18Z"
+ expires_at: "2023-05-20T00:54:18Z"
+ last_used_at: "2022-06-15T12:30:45Z"
getOrgAccessTokenResponse:
+ examples:
+ - id: a7a5ef25-8889-43a0-8cc7-f2a94268e861
+ label: My organization token
+ created_by: johndoe
+ is_active: true
+ created_at: "2022-05-20T00:54:18Z"
+ expires_at: "2023-05-20T00:54:18Z"
+ last_used_at: "2022-06-15T12:30:45Z"
+ resources:
+ - type: TYPE_REPO
+ path: myorg/myrepo
+ scopes:
+ - scope-image-pull
allOf:
- $ref: "#/components/schemas/orgAccessToken"
- type: object
@@ -4125,77 +4695,111 @@ components:
items:
$ref: "#/components/schemas/orgAccessTokenResource"
createOrgAccessTokenRequest:
+ examples:
+ - label: My organization token
+ description: Token for CI/CD pipeline
+ resources:
+ - type: TYPE_REPO
+ path: myorg/myrepo
+ scopes:
+ - scope-image-pull
type: object
properties:
label:
type: string
description: Label for the access token
- example: "My organization token"
- required: true
+ example: My organization token
description:
type: string
description: Description of the access token
- example: "Token for CI/CD pipeline"
+ example: Token for CI/CD pipeline
resources:
type: array
description: Resources this token has access to
items:
$ref: "#/components/schemas/orgAccessTokenResource"
expires_at:
- type: string
+ type:
+ - string
+ - "null"
format: date-time
description: Expiration date for the token
- example: "2023-05-20T00:54:18Z"
- nullable: true
+ example: 2023-05-20T00:54:18Z
+ required:
+ - label
createOrgAccessTokenResponse:
+ examples:
+ - id: a7a5ef25-8889-43a0-8cc7-f2a94268e861
+ label: My organization token
+ is_active: true
+ created_at: "2022-05-20T00:54:18Z"
+ expires_at: "2023-05-20T00:54:18Z"
+ last_used_at: "2022-06-15T12:30:45Z"
+ token: dckr_oat_7awgM4jG5SQvxcvmNzhKj8PQjxo
+ resources:
+ - type: TYPE_REPO
+ path: myorg/myrepo
+ scopes:
+ - scope-image-pull
type: object
allOf:
- type: object
properties:
id:
type: string
- example: "a7a5ef25-8889-43a0-8cc7-f2a94268e861"
+ example: a7a5ef25-8889-43a0-8cc7-f2a94268e861
label:
type: string
- example: "My organization token"
+ example: My organization token
is_active:
type: boolean
example: true
created_at:
type: string
format: date-time
- example: "2022-05-20T00:54:18Z"
+ example: 2022-05-20T00:54:18Z
expires_at:
- type: string
+ type:
+ - string
+ - "null"
format: date-time
- example: "2023-05-20T00:54:18Z"
- nullable: true
+ example: 2023-05-20T00:54:18Z
last_used_at:
- type: string
+ type:
+ - string
+ - "null"
format: date-time
- example: "2022-06-15T12:30:45Z"
- nullable: true
+ example: 2022-06-15T12:30:45Z
- type: object
properties:
token:
type: string
description: The actual token value that can be used for authentication
- example: "dckr_oat_7awgM4jG5SQvxcvmNzhKj8PQjxo"
+ example: dckr_oat_7awgM4jG5SQvxcvmNzhKj8PQjxo
resources:
type: array
items:
$ref: "#/components/schemas/orgAccessTokenResource"
updateOrgAccessTokenRequest:
+ examples:
+ - label: My organization token
+ description: Token for CI/CD pipeline
+ resources:
+ - type: TYPE_REPO
+ path: myorg/myrepo
+ scopes:
+ - scope-image-pull
+ is_active: true
type: object
properties:
label:
type: string
description: Label for the access token
- example: "My organization token"
+ example: My organization token
description:
type: string
description: Description of the access token
- example: "Token for CI/CD pipeline"
+ example: Token for CI/CD pipeline
resources:
type: array
description: Resources this token has access to
@@ -4206,6 +4810,19 @@ components:
description: Whether the token is active
example: true
updateOrgAccessTokenResponse:
+ examples:
+ - id: a7a5ef25-8889-43a0-8cc7-f2a94268e861
+ label: My organization token
+ created_by: johndoe
+ is_active: true
+ created_at: "2022-05-20T00:54:18Z"
+ expires_at: "2023-05-20T00:54:18Z"
+ last_used_at: "2022-06-15T12:30:45Z"
+ resources:
+ - type: TYPE_REPO
+ path: myorg/myrepo
+ scopes:
+ - scope-image-pull
type: object
allOf:
- $ref: "#/components/schemas/orgAccessToken"
@@ -4216,34 +4833,23 @@ components:
description: Resources this token has access to
items:
$ref: "#/components/schemas/orgAccessTokenResource"
- team_repo:
- allOf:
- - $ref: "#/components/responses/team_repo"
- properties:
- group_name:
- type: string
- description: Name of the group
- permission:
- type: string
- description: Repo access permission
- enum:
- - read
- - write
- - admin
parameters:
namespace:
+ description: Namespace of the repository, such as a user or organization name.
in: path
name: namespace
required: true
schema:
type: string
repository:
+ description: Name of the repository within the namespace.
in: path
name: repository
required: true
schema:
type: string
tag:
+ description: Name of the image tag.
in: path
name: tag
required: true
@@ -4313,6 +4919,7 @@ components:
example: d80f7c79-7730-49d8-9a41-7c42fb622d9c
required: true
type:
+ description: Filter members by membership type.
in: query
name: type
schema:
@@ -4323,6 +4930,7 @@ components:
- member
example: all
role:
+ description: Filter members by their organization role.
in: query
name: role
schema:
@@ -4344,6 +4952,15 @@ components:
content:
application/json:
schema:
+ examples:
+ - org: docker
+ team: owners
+ role: member
+ invitees:
+ - invitee1DockerId
+ - invitee2@docker.com
+ - invitee3@docker.com
+ dry_run: true
type: object
required:
- org
@@ -4380,6 +4997,13 @@ components:
content:
application/scim+json:
schema:
+ examples:
+ - schemas:
+ - urn:ietf:params:scim:schemas:core:2.0:User
+ userName: jon.snow@docker.com
+ name:
+ givenName: Jon
+ familyName: Snow
type: object
required:
- schemas
@@ -4399,22 +5023,33 @@ components:
type: object
required:
- schemas
+ - userName
properties:
schemas:
$ref: "#/components/schemas/scim_user_schemas"
name:
- allOf:
- - $ref: "#/components/schemas/scim_user_name"
- - description: If this is omitted from the request, the update will skip the update on it. We will only ever change the name, but not clear it.
- enabled:
+ $ref: "#/components/schemas/scim_user_name"
+ userName:
+ $ref: "#/components/schemas/scim_user_username"
+ active:
type: boolean
default: false
- description: If this is omitted from the request, it will default to false resulting in a deactivated user.
+ description: Whether the user is active. Defaults to false when omitted.
+ examples:
+ - schemas:
+ - urn:ietf:params:scim:schemas:core:2.0:User
+ userName: jon.snow@docker.com
+ name:
+ givenName: Jon
+ familyName: Snow
+ active: true
add_member_to_org_group:
required: true
content:
application/json:
schema:
+ examples:
+ - member: jonsnow
type: object
required:
- member
@@ -4442,6 +5077,10 @@ components:
bearerSCIMAuth:
type: http
scheme: bearer
+ scimToken:
+ type: http
+ scheme: bearer
+ description: Use the SCIM provisioning token configured for the organization.
x-tagGroups:
- name: General
tags:
@@ -4462,3 +5101,5 @@ x-tagGroups:
- org-access-tokens
- groups
- invites
+jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
+security: []
diff --git a/content/reference/api/registry/latest.md b/content/reference/api/registry/latest.md
deleted file mode 100644
index 7df6e5971659..000000000000
--- a/content/reference/api/registry/latest.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-layout: api
-title: Supported registry API for Docker Hub
-linktitle: Latest
-description: "Supported registry API endpoints."
-keywords: registry, on-prem, images, tags, repository, distribution, api, advanced
----
diff --git a/content/reference/api/registry/latest.yaml b/content/reference/api/registry/latest.yaml
index 445cc7f98f95..e28bb4506f9c 100644
--- a/content/reference/api/registry/latest.yaml
+++ b/content/reference/api/registry/latest.yaml
@@ -1,4 +1,4 @@
-openapi: 3.0.3
+openapi: 3.2.0
info:
title: Supported registry API for Docker Hub
description: |
@@ -11,14 +11,13 @@ info:
It focuses on pulling, pushing, and deleting images. It does not cover the full OCI Distribution Specification.
For the complete OCI specification, see [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec).
+ version: "2"
servers:
- description: Docker Hub registry API
x-audience: public
url: https://registry-1.docker.io
-
tags:
- name: overview
- x-displayName: Overview
description: |
All endpoints in this API are prefixed by the version and repository name, for example:
@@ -38,29 +37,28 @@ tags:
1. Consist of path components matching `[a-z0-9]+(?:[._-][a-z0-9]+)*`
2. If more than one component, they must be separated by `/`
3. Full repository name must be fewer than 256 characters
-
-
+ summary: Overview
+ kind: info
- name: authentication
- x-displayName: Authentication
description: |
Specifies registry authentication.
externalDocs:
description: Detailed authentication workflow and token usage
url: https://docs.docker.com/reference/api/registry/auth/
-
+ summary: Authentication
+ kind: info
- name: Manifests
- x-displayName: Manifests
description: |
Image manifests are JSON documents that describe an image: its configuration blob, the digests of each layer blob, and metadata such as media‑types and annotations.
-
+ summary: Manifests
+ kind: nav
- name: Blobs
- x-displayName: Blobs
description: |
Blobs are the binary objects referenced from manifests:
the config JSON and one or more compressed layer tarballs.
-
+ summary: Blobs
+ kind: nav
- name: pull
- x-displayName: Pulling Images
description: |
Pulling an image involves retrieving the manifest and downloading each of the image's layer blobs. This section outlines the general steps followed by a working example.
@@ -79,94 +77,93 @@ tags:
# Step 1: Get a bearer token
TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/ubuntu:pull" | jq -r .token)
-
+
# Step 2: Get the image manifest. In this example, an image manifest list is returned.
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
https://registry-1.docker.io/v2/library/ubuntu/manifests/latest \
-o manifest-list.json
-
+
# Step 3a: Parse the `manifests[]` array to locate the digest for your target platform (e.g., `linux/amd64`).
IMAGE_MANIFEST_DIGEST=$(jq -r '.manifests[] | select(.platform.architecture == "amd64" and .platform.os == "linux") | .digest' manifest-list.json)
-
+
# Step 3b: Get the platform-specific image manifest
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://registry-1.docker.io/v2/library/ubuntu/manifests/$IMAGE_MANIFEST_DIGEST \
-o manifest.json
-
+
# Step 4: Send a HEAD request to check if the layer blob exists
DIGEST=$(jq -r '.layers[0].digest' manifest.json)
curl -I -H "Authorization: Bearer $TOKEN" \
https://registry-1.docker.io/v2/library/ubuntu/blobs/$DIGEST
-
+
# Step 5: Download the layer blob
curl -L -H "Authorization: Bearer $TOKEN" \
https://registry-1.docker.io/v2/library/ubuntu/blobs/$DIGEST
```
-
- This example pulls the manifest and the first layer for the `ubuntu:latest` image on the `linux/amd64` platform. Repeat steps 4 and 5 for each digest in the `.layers[]` array in the manifest.
-
+ This example pulls the manifest and the first layer for the `ubuntu:latest` image on the `linux/amd64` platform. Repeat steps 4 and 5 for each digest in the `.layers[]` array in the manifest.
+ summary: Pulling Images
+ kind: info
- name: push
- x-displayName: Pushing Images
description: |
Pushing an image involves uploading any image blobs (such as the config or layers), and then uploading the manifest that references those blobs.
-
+
This section outlines the basic steps to push an image using the registry API.
-
+
1. [Get a bearer token for the repository](https://docs.docker.com/reference/api/registry/auth/)
-
+
2. [Check if the blob exists](#operation/CheckBlobExists) using a `HEAD` request for each blob digest.
-
+
3. If the blob does not exist, [upload the blob](#operation/CompleteBlobUpload) using a monolithic `PUT` request:
- First, [initiate the upload](#operation/InitiateBlobUpload) with `POST`.
- Then [upload and complete](#operation/CompleteBlobUpload) with `PUT`.
**Note**: Alternatively, you can upload the blob in multiple chunks by using `PATCH` requests to send each chunk, followed by a final `PUT` request to complete the upload. This is known as a [chunked upload](#operation/UploadBlobChunk) and is useful for large blobs or when resuming interrupted uploads.
-
+
4. [Upload the image manifest](#operation/PutImageManifest) using a `PUT` request to associate the config and layers.
-
+
The following bash script example pushes a dummy config blob and manifest to `yourusername/helloworld:latest` on Docker Hub. You can replace `yourusername` with your Docker Hub username and `dckr_pat` with your Docker Hub personal access token.
-
+
```bash
#!/bin/bash
-
+
USERNAME=yourusername
PASSWORD=dckr_pat
REPO=yourusername/helloworld
TAG=latest
CONFIG=config.json
MIME_TYPE=application/vnd.docker.container.image.v1+json
-
+
# Step 1: Get a bearer token
TOKEN=$(curl -s -u "$USERNAME:$PASSWORD" \
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:$REPO:push,pull" \
| jq -r .token)
-
+
# Create a dummy config blob and compute its digest
echo '{"architecture":"amd64","os":"linux","config":{},"rootfs":{"type":"layers","diff_ids":[]}}' > $CONFIG
DIGEST="sha256:$(sha256sum $CONFIG | awk '{print $1}')"
-
+
# Step 2: Check if the blob exists
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -I \
-H "Authorization: Bearer $TOKEN" \
https://registry-1.docker.io/v2/$REPO/blobs/$DIGEST)
-
+
if [ "$STATUS" != "200" ]; then
# Step 3: Upload blob using monolithic upload
LOCATION=$(curl -sI -X POST \
-H "Authorization: Bearer $TOKEN" \
https://registry-1.docker.io/v2/$REPO/blobs/uploads/ \
| grep -i Location | tr -d '\r' | awk '{print $2}')
-
+
curl -s -X PUT "$LOCATION&digest=$DIGEST" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @$CONFIG
fi
-
+
# Step 4: Upload the manifest that references the config blob
MANIFEST=$(cat <` header.
-
+
+ Use the `Accept` header to select the manifest representation. Supported media types:
+
+ - `application/vnd.docker.distribution.manifest.v2+json`
+ - `application/vnd.docker.distribution.manifest.list.v2+json`
+ - `application/vnd.oci.image.manifest.v1+json`
+ - `application/vnd.oci.image.index.v1+json`
x-codeSamples:
- lang: Bash
label: cURL
@@ -289,26 +292,6 @@ paths:
value: sha256:abc123def456...
schema:
type: string
- - name: Authorization
- in: header
- required: true
- description: RFC7235-compliant authorization header (e.g., `Bearer `).
- schema:
- type: string
- - name: Accept
- in: header
- required: false
- description: |
- Media type(s) the client supports for the manifest.
-
- The registry supports the following media types:
- - application/vnd.docker.distribution.manifest.v2+json
- - application/vnd.docker.distribution.manifest.list.v2+json
- - application/vnd.oci.image.manifest.v1+json
- - application/vnd.oci.image.index.v1+json
- schema:
- type: string
-
responses:
"200":
description: Manifest fetched successfully.
@@ -367,28 +350,19 @@ paths:
docker-manifest:
summary: Docker image manifest (schema v2)
value:
- {
- "schemaVersion": 2,
- "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
- "config": {
- "mediaType": "application/vnd.docker.container.image.v1+json",
- "size": 7023,
- "digest": "sha256:123456abcdef..."
- },
- "layers": [
- {
- "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
- "size": 32654,
- "digest": "sha256:abcdef123456..."
- },
- {
- "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
- "size": 16724,
- "digest": "sha256:7890abcdef12..."
- }
- ]
- }
-
+ schemaVersion: 2
+ mediaType: application/vnd.docker.distribution.manifest.v2+json
+ config:
+ mediaType: application/vnd.docker.container.image.v1+json
+ size: 7023
+ digest: sha256:123456abcdef...
+ layers:
+ - mediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
+ size: 32654
+ digest: sha256:abcdef123456...
+ - mediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
+ size: 16724
+ digest: sha256:7890abcdef12...
"400":
description: Invalid name or reference.
"401":
@@ -399,8 +373,6 @@ paths:
description: Repository or manifest not found.
"429":
description: Too many requests.
-
-
put:
tags:
- Manifests
@@ -446,20 +418,6 @@ paths:
value: sha256:abc123def456...
schema:
type: string
- - name: Authorization
- in: header
- required: true
- description: RFC7235-compliant authorization header (e.g., `Bearer `).
- schema:
- type: string
- - name: Content-Type
- in: header
- required: true
- description: Media type of the manifest being uploaded.
- schema:
- type: string
- example: application/vnd.docker.distribution.manifest.v2+json
-
requestBody:
required: true
content:
@@ -512,28 +470,20 @@ paths:
digest:
type: string
example: sha256:abcdef123456...
-
examples:
sample-manifest:
summary: Sample Docker image manifest (schema v2)
value:
- {
- "schemaVersion": 2,
- "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
- "config": {
- "mediaType": "application/vnd.docker.container.image.v1+json",
- "size": 7023,
- "digest": "sha256:123456abcdef..."
- },
- "layers": [
- {
- "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
- "size": 32654,
- "digest": "sha256:abcdef123456..."
- }
- ]
- }
-
+ schemaVersion: 2
+ mediaType: application/vnd.docker.distribution.manifest.v2+json
+ config:
+ mediaType: application/vnd.docker.container.image.v1+json
+ size: 7023
+ digest: sha256:123456abcdef...
+ layers:
+ - mediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
+ size: 32654
+ digest: sha256:abcdef123456...
responses:
"201":
description: Manifest created successfully.
@@ -579,6 +529,12 @@ paths:
This endpoint requires authentication with pull scope.
+ Use the `Accept` header to select the manifest representation. Supported media types:
+
+ - `application/vnd.docker.distribution.manifest.v2+json`
+ - `application/vnd.docker.distribution.manifest.list.v2+json`
+ - `application/vnd.oci.image.manifest.v1+json`
+ - `application/vnd.oci.image.index.v1+json`
parameters:
- name: name
in: path
@@ -600,20 +556,6 @@ paths:
value: sha256:abc123def456...
schema:
type: string
- - name: Authorization
- in: header
- required: true
- schema:
- type: string
- description: Bearer token for authentication
- - name: Accept
- in: header
- required: false
- schema:
- type: string
- example: application/vnd.docker.distribution.manifest.v2+json
- description: |
- Media type of the manifest to check. The response will match one of the accepted types.
x-codeSamples:
- lang: Bash
label: cURL
@@ -642,12 +584,12 @@ paths:
schema:
type: string
example: application/vnd.docker.distribution.manifest.v2+json
- "404":
- description: Manifest not found.
"401":
description: Authentication required.
"403":
description: Access denied.
+ "404":
+ description: Manifest not found.
"429":
description: Too many requests.
delete:
@@ -680,12 +622,6 @@ paths:
example: sha256:abc123def456...
schema:
type: string
- - name: Authorization
- in: header
- required: true
- description: Bearer token with `delete` access
- schema:
- type: string
x-codeSamples:
- lang: Bash
label: cURL
@@ -733,7 +669,6 @@ paths:
curl -i -X POST \
-H "Authorization: Bearer $TOKEN" \
https://registry-1.docker.io/v2/library/ubuntu/blobs/uploads/
-
- lang: Bash
label: cURL (Cross-Repository Blob Mount)
source: |
@@ -741,7 +676,6 @@ paths:
curl -i -X POST \
-H "Authorization: Bearer $TOKEN" \
"https://registry-1.docker.io/v2/library/ubuntu/blobs/uploads/?mount=sha256:abc123def456...&from=library/busybox"
-
parameters:
- name: name
in: path
@@ -764,13 +698,6 @@ paths:
schema:
type: string
example: library/busybox
- - name: Authorization
- in: header
- required: true
- schema:
- type: string
- description: Bearer token for authentication with `push` scope
-
responses:
"201":
description: Blob successfully mounted from another repository.
@@ -858,14 +785,6 @@ paths:
schema:
type: string
example: sha256:abc123def4567890...
- - name: Authorization
- in: header
- required: true
- description: Bearer token with pull or push scope
- schema:
- type: string
- example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6...
-
responses:
"200":
description: Blob exists
@@ -885,32 +804,12 @@ paths:
schema:
type: string
example: application/octet-stream
- content:
- application/json:
- examples:
- blob-check-request:
- summary: Sample request
- value:
- method: HEAD
- url: /v2/library/ubuntu/blobs/sha256:abc123def4567890...
- headers:
- Authorization: Bearer
- Accept: '*/*'
- blob-check-response:
- summary: Sample 200 response headers
- value:
- status: 200 OK
- headers:
- Docker-Content-Digest: sha256:abc123def4567890...
- Content-Length: 32654
- Content-Type: application/octet-stream
-
- "404":
- description: Blob not found
"401":
description: Authentication required
"403":
description: Access denied
+ "404":
+ description: Blob not found
"429":
description: Too many requests
get:
@@ -950,14 +849,6 @@ paths:
schema:
type: string
example: sha256:abc123def456...
- - name: Authorization
- in: header
- required: true
- schema:
- type: string
- description: Bearer token with pull scope
- example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6...
-
responses:
"200":
description: Blob content returned directly
@@ -985,8 +876,7 @@ paths:
examples:
small-layer:
summary: Example binary blob (gzipped tar layer)
- value: ""
-
+ value:
"307":
description: Temporary redirect to blob location
headers:
@@ -1023,7 +913,7 @@ paths:
label: cURL
source: |
# GET upload status
- curl -I \
+ curl --include --request GET \
-H "Authorization: Bearer $TOKEN" \
https://registry-1.docker.io/v2/library/ubuntu/blobs/uploads/abc123
parameters:
@@ -1031,7 +921,7 @@ paths:
in: path
required: true
description: Repository Name
- example : library/ubuntu
+ example: library/ubuntu
schema:
type: string
- name: uuid
@@ -1041,13 +931,6 @@ paths:
schema:
type: string
example: abc123
- - name: Authorization
- in: header
- required: true
- schema:
- type: string
- example: Bearer eyJhbGciOi...
-
responses:
"204":
description: Upload in progress. No body is returned.
@@ -1075,7 +958,6 @@ paths:
description: Upload session not found
"429":
description: Too many requests
-
put:
tags:
- Blobs
@@ -1089,7 +971,6 @@ paths:
This endpoint supports:
- Monolithic uploads (upload entire blob in this request)
- Finalizing chunked uploads (last chunk plus `digest`)
-
x-codeSamples:
- lang: Bash
label: cURL
@@ -1100,8 +981,6 @@ paths:
-H "Content-Type: application/octet-stream" \
--data-binary @layer.tar.gz \
"https://registry-1.docker.io/v2/library/ubuntu/blobs/uploads/abc123?digest=sha256:abcd1234..."
-
-
parameters:
- name: name
in: path
@@ -1124,13 +1003,6 @@ paths:
schema:
type: string
example: sha256:abcd1234...
- - name: Authorization
- in: header
- required: true
- schema:
- type: string
- example: Bearer eyJhbGciOi...
-
requestBody:
required: false
content:
@@ -1141,8 +1013,7 @@ paths:
examples:
layer-upload:
summary: Layer tarball blob
- value: ""
-
+ value:
responses:
"201":
description: Upload completed successfully
@@ -1174,7 +1045,6 @@ paths:
description: Requested range not satisfiable (if used in chunked mode)
"429":
description: Too many requests
-
patch:
tags:
- Blobs
@@ -1216,12 +1086,6 @@ paths:
schema:
type: string
example: abc123
- - name: Authorization
- in: header
- required: true
- schema:
- type: string
- example: Bearer eyJhbGciOi...
- name: Content-Range
in: header
required: false
@@ -1229,7 +1093,6 @@ paths:
type: string
example: bytes 0-65535
description: Optional. Byte range of the chunk being sent
-
requestBody:
required: true
content:
@@ -1240,8 +1103,7 @@ paths:
examples:
chunk-0:
summary: Upload chunk 0 of a blob
- value: ""
-
+ value:
responses:
"202":
description: Chunk accepted and stored
@@ -1288,7 +1150,6 @@ paths:
- The client wants to clean up unused upload sessions
After cancellation, the UUID is no longer valid and a new `POST` must be issued to restart the upload.
-
x-codeSamples:
- lang: Bash
label: cURL
@@ -1297,7 +1158,6 @@ paths:
curl -X DELETE \
-H "Authorization: Bearer $TOKEN" \
https://registry-1.docker.io/v2/library/ubuntu/blobs/uploads/abc123`
-
parameters:
- name: name
in: path
@@ -1313,13 +1173,6 @@ paths:
schema:
type: string
example: abc123
- - name: Authorization
- in: header
- required: true
- schema:
- type: string
- example: Bearer eyJhbGciOi...
-
responses:
"204":
description: Upload session cancelled successfully. No body is returned.
@@ -1337,8 +1190,6 @@ paths:
description: Upload session not found
"429":
description: Too many requests
-
-
x-tagGroups:
- name: General
tags:
@@ -1351,3 +1202,12 @@ x-tagGroups:
tags:
- Manifests
- Blobs
+jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
+components:
+ securitySchemes:
+ registryToken:
+ type: http
+ scheme: bearer
+ description: Follow the WWW-Authenticate challenge and obtain a repository-scoped registry bearer token. Public image pulls can obtain a token without account credentials; the registry request still sends that token. This token exchange is separate from Hub API authentication.
+security:
+ - registryToken: []
diff --git a/hack/api-docs/README.md b/hack/api-docs/README.md
new file mode 100644
index 000000000000..5288e522477a
--- /dev/null
+++ b/hack/api-docs/README.md
@@ -0,0 +1,83 @@
+# API reference build
+
+This tool validates the Hub, DVP, and Registry OpenAPI sources in
+`content/reference/api/` and generates presentation data for Hugo. Engine and
+AI Governance retain their existing sources and renderers.
+
+## Commands
+
+Use the Go version declared in `go.mod`, Hugo, and the site's Node dependencies.
+From the repository root:
+
+```console
+$ npm ci
+$ ./hack/api-docs/run.sh test
+$ ./hack/api-docs/run.sh generate
+$ hugo server
+```
+
+For a static build with HTML/Markdown checks, run
+`./hack/api-docs/run.sh build`. To serve that build on port 1314, run
+`./hack/api-docs/run.sh serve`. Set `DOCS_URL` and `DOCS_PORT` when using another
+address. Run generation again after changing a specification or source manifest entry.
+
+Docker builds and Netlify deploy previews run generation before Hugo. Generated
+data, validation reports, binaries, and local builds go under `tmp/api-reference/`.
+Hugo reports an error if the generated data is absent.
+
+## Processor inputs
+
+`sources.json` registers input specifications and their product/manual
+relationships. Both generation and the validation wrapper use this manifest.
+Hugo reads the generated presentation data, rather than this file. Authentication
+descriptions come from the specifications and linked guides.
+
+`validation/` contains the Vacuum rules and locked official
+schema resources. `testdata/` contains validation fixtures. The presentation model
+version and supported dialect are defined by the processor, not configurable
+manifest fields.
+
+## Pipeline
+
+1. Parse the authoritative YAML with `libopenapi` and preserve source values and
+ reference identities. Verify locked OpenAPI dialect resources.
+2. Run Vacuum policy checks and JSON Schema validation of schemas and supplied
+ examples, followed by documentation profile checks.
+3. Generate presentation model version 1: operations, effective security and
+ servers, parameters, media variants, examples, schema links, and provenance.
+4. Render HTML and Markdown through the content adapter and `api-docs` templates.
+5. Run the shared site flattening script, then check generated operation,
+ parameter, media, schema, and link coverage.
+
+The shared `hack/flatten-and-resolve.js` script resolves Markdown link destinations
+relative to each original file, then moves `index.md` files to flattened paths.
+It preserves code examples and other text. API pages use the same processing as
+other pages; links that already use published URLs remain unchanged.
+
+The published YAML URLs still serve the source files directly. There is no
+conversion step, snapshot dependency, Node migration package, or source archive
+in the build.
+
+## Validation
+
+`check` is strict and reports every documentation profile failure:
+
+```console
+$ ./hack/api-docs/run.sh check
+```
+
+Generation runs the same strict validation. Any diagnostic fails the build;
+there is no exception baseline. Reports are written to
+`tmp/api-reference/validation.json`.
+
+## Tests and scope
+
+Go fixtures cover dialects, references, recursion, boolean schemas, examples,
+security overrides, server and parameter precedence, and request generation.
+`verify-output.mjs` checks all generated HTML/Markdown pairs and retention of
+Engine v1.40–v1.56 in ReDoc, unchanged Governance rendering, and byte-identical
+published specifications. `browser-checks.mjs` exports a Playwright check for
+navigation, page aliases, filtering, requests, and narrow screens.
+
+Callbacks and webhook navigation are unsupported and fail validation. Request
+examples are POSIX shell templates; they do not make service calls.
diff --git a/hack/api-docs/browser-checks.mjs b/hack/api-docs/browser-checks.mjs
new file mode 100644
index 000000000000..a5eb05d5b614
--- /dev/null
+++ b/hack/api-docs/browser-checks.mjs
@@ -0,0 +1,106 @@
+// Run with a Playwright Page against a built site; no product API calls.
+export default async function verify(page, base = "http://localhost:1314") {
+ const results = [];
+ const assert = (condition, message) => {
+ if (!condition) throw Error(message);
+ results.push(message);
+ };
+ await page.setViewportSize({ width: 1440, height: 1000 });
+ await page.goto(base + "/reference/api/");
+ assert(
+ (await page.locator(".api-card").count()) === 5,
+ "Five API catalog entries",
+ );
+ assert(
+ (await page.locator("nav.navbar-font").count()) === 1,
+ "Catalog retains Reference sidebar",
+ );
+ await page.goto(base + "/reference/api/dvp/latest/");
+ assert(
+ (await page.locator(".api-nav").count()) === 1 &&
+ (await page.locator("nav.navbar-font").count()) === 0,
+ "Reference uses local navigation",
+ );
+ const first = page.locator("[data-api-filter-item]").first();
+ const operationURL = await first.getAttribute("href");
+ await page.goto(base + "/reference/api/hub/dvp/");
+ await page.waitForURL("**/reference/api/dvp/latest/");
+ assert(
+ page.url().endsWith("/reference/api/dvp/latest/"),
+ "DVP alias reaches overview",
+ );
+ await page.goto(base + operationURL);
+ await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
+ const copy = page.locator("[data-api-copy]");
+ await copy.focus();
+ await page.keyboard.press("Enter");
+ await page.waitForFunction(
+ () => document.querySelector("[data-api-copy]").textContent === "Copied",
+ );
+ assert(
+ (await page.evaluate(() => navigator.clipboard.readText())).includes(
+ "curl",
+ ),
+ "Keyboard copies a request",
+ );
+ await page.locator(".api-nav-back").focus();
+ await page.keyboard.press("Enter");
+ await page.waitForURL("**/reference/api/");
+ assert(
+ (await page.locator("nav.navbar-font").count()) === 1,
+ "Keyboard back link restores catalog",
+ );
+ await page.goto(base + "/reference/api/hub/latest/");
+ await page.locator("[data-api-filter]").fill("token");
+ const rows = page.locator("[data-api-filter-item]:visible");
+ assert(
+ (await rows.count()) > 0 &&
+ (await rows.allTextContents()).every((x) =>
+ x.toLowerCase().includes("token"),
+ ),
+ "Operation filtering",
+ );
+ const schemaURL = await page
+ .locator(".api-schema-links a")
+ .first()
+ .getAttribute("href");
+ await page.goto(base + schemaURL);
+ assert(
+ (await page.locator('[data-api-view="schema"]').count()) === 1,
+ "Linked schema page",
+ );
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto(base + operationURL);
+ assert(
+ await page.evaluate(
+ () => document.documentElement.scrollWidth <= innerWidth + 1,
+ ),
+ "No narrow-screen document overflow",
+ );
+ await page.goto(base + "/reference/api/engine/version/v1.56/");
+ assert(
+ (await page.locator("redoc").count()) === 1,
+ "Latest Engine retains ReDoc",
+ );
+ await page.goto(base + "/reference/api/ai-governance/");
+ assert(
+ (await page.locator("[data-api-view]").count()) === 0 &&
+ (await page.locator("h1").count()) > 0,
+ "Governance retains existing renderer",
+ );
+ const context = await page
+ .context()
+ .browser()
+ .newContext({ javaScriptEnabled: false });
+ try {
+ const staticPage = await context.newPage();
+ await staticPage.goto(base + operationURL);
+ assert(
+ (await staticPage.locator("[data-api-copy-source]").count()) === 1,
+ "Request content exists without JavaScript",
+ );
+ } finally {
+ await context.close();
+ }
+ return results;
+}
diff --git a/hack/api-docs/contracts_test.go b/hack/api-docs/contracts_test.go
new file mode 100644
index 000000000000..9163401c934f
--- /dev/null
+++ b/hack/api-docs/contracts_test.go
@@ -0,0 +1,125 @@
+package main
+
+import (
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func localDocument(t *testing.T, api string) *Document {
+ t.Helper()
+ d, err := loadDocument(filepath.Join("..", "..", "content", "reference", "api", api, "latest.yaml"), "validation/dialects")
+ if err != nil {
+ t.Fatal(err)
+ }
+ d.Source = Source{ID: api}
+ d.validate("")
+ if len(d.Diagnostics) != 0 {
+ t.Fatalf("%s must pass strict validation: %+v", api, d.Diagnostics)
+ }
+ return d
+}
+
+func TestHubResponseContracts(t *testing.T) {
+ d := localDocument(t, "hub")
+ for _, tc := range []struct {
+ pointer, value string
+ valid bool
+ }{
+ {"/components/schemas/error", `{"message":"not found","errinfo":null}`, true},
+ {"/components/schemas/error", `{"errinfo":{"field":["invalid"],"limit":5}}`, true},
+ {"/components/schemas/error", `{"errinfo":[]}`, false},
+ {"/components/schemas/getAccessTokensResponse", `{"next":null,"previous":null,"results":[]}`, true},
+ {"/components/schemas/getOrgAccessTokensResponse", `{"next":null,"previous":null,"results":[]}`, true},
+ {"/components/schemas/org_member_paginated", `{"count":0,"next":null,"previous":null,"results":[]}`, true},
+ {"/components/schemas/org_member_paginated", `[]`, false},
+ {"/components/schemas/bulk_invite", `{"invitees":[]}`, true},
+ {"/components/schemas/bulk_invite", `{"invitees":{"invitees":[]}}`, false},
+ {"/components/schemas/tag", `{"v2":true,"images":[{"architecture":"amd64","variant":null,"features":null,"os_features":null,"os_version":null}]}`, true},
+ {"/components/schemas/tag", `{"v2":"true","images":{}}`, false},
+ {"/components/schemas/immutable_tags_verify_request", `{"regex":"v.*"}`, true},
+ {"/components/schemas/immutable_tags_verify_request", `{"regex":"v1,v2"}`, false},
+ {"/components/schemas/scim_service_provider_config", `{"authenticationSchemes":[{"type":"oauthbearertoken"}]}`, true},
+ {"/components/schemas/scim_service_provider_config", `{"authenticationSchemes":{}}`, false},
+ {"/components/requestBodies/scim_update_user_request/content/application~1scim+json/schema", `{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"user@example.com","active":false}`, true},
+ {"/components/requestBodies/scim_update_user_request/content/application~1scim+json/schema", `{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"]}`, false},
+ {"/paths/~1v2~1orgs~1{org_name}~1members~1export/get/responses/200/content/text~1csv/schema", `"Name,Username\nUser,dockeruser\n"`, true},
+ {"/paths/~1v2~1orgs~1{org_name}~1members~1export/get/responses/200/content/text~1csv/schema", `[{"Name":"User"}]`, false},
+ } {
+ t.Run(tc.pointer+"/"+tc.value, func(t *testing.T) {
+ s, err := d.Compiler.Compile(d.URI + "#" + tc.pointer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := s.Validate(jsonNumber(tc.value)); (err == nil) != tc.valid {
+ t.Fatalf("valid = %t, want %t: %v", err == nil, tc.valid, err)
+ }
+ })
+ }
+ for _, response := range []string{"scim_get_resource_types_resp", "scim_get_schemas_resp", "scim_get_users_resp"} {
+ v, err := pointer(d.Root, "/components/responses/"+response+"/content/application~1scim+json/schema/properties")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if obj(v)["Resources"] == nil || obj(v)["resources"] != nil {
+ t.Fatalf("incorrect SCIM list field casing: %s", response)
+ }
+ }
+}
+
+func TestRegistryRequests(t *testing.T) {
+ d := localDocument(t, "registry")
+ for _, raw := range arr(d.model()["operations"]) {
+ op := obj(raw)
+ curl := str(op["curl"])
+ if strings.Count(curl, "Authorization:") != 1 || !strings.Contains(curl, "${REGISTRY_TOKEN}") {
+ t.Errorf("incorrect registry authentication: %s", curl)
+ }
+ if strings.Count(curl, "Content-Type:") > 1 {
+ t.Errorf("duplicate content type: %s", curl)
+ }
+ if (op["id"] == "GetImageManifest" || op["id"] == "HeadImageManifest") && !strings.Contains(curl, "Accept: application/vnd.docker.distribution.manifest.v2+json") {
+ t.Errorf("missing manifest negotiation: %s", curl)
+ }
+ if op["id"] == "GetBlobUploadStatus" && !strings.Contains(curl, "--request GET") {
+ t.Errorf("incorrect upload status method: %s", curl)
+ }
+ }
+}
+
+func TestReservedHeaderParameters(t *testing.T) {
+ for _, name := range []string{"Authorization", "accept", "CONTENT-TYPE", "Content-Range"} {
+ t.Run(name, func(t *testing.T) {
+ d := fixture(t, "valid.yaml")
+ op := obj(obj(obj(d.Root["paths"])["/public"])["get"])
+ op["parameters"] = []any{Object{"name": name, "in": "header", "description": "Header value.", "schema": Object{"type": "string"}}}
+ d.validate("")
+ found := false
+ for _, diag := range d.Diagnostics {
+ found = found || (diag.Rule == "S8" && strings.Contains(diag.Message, "ignores"))
+ }
+ if found != (name != "Content-Range") {
+ t.Fatalf("unexpected header validation: %+v", d.Diagnostics)
+ }
+ })
+ }
+}
+
+func TestExampleText(t *testing.T) {
+ for _, tc := range []struct {
+ media string
+ value any
+ text, language string
+ }{
+ {"text/csv", "Name,Username\nUser,dockeruser\n", "Name,Username\nUser,dockeruser\n", "text"},
+ {"text/plain", "&\n", "&\n", "text"},
+ {"application/json", "a\nb", `"a\nb"`, "json"},
+ {"application/scim+json", false, "false", "json"},
+ {"application/json", jsonNumber("0"), "0", "json"},
+ } {
+ text, language := exampleText(tc.media, tc.value)
+ if text != tc.text || language != tc.language {
+ t.Errorf("got %q (%s), want %q (%s)", text, language, tc.text, tc.language)
+ }
+ }
+}
diff --git a/hack/api-docs/go.mod b/hack/api-docs/go.mod
new file mode 100644
index 000000000000..6f45e2016a8f
--- /dev/null
+++ b/hack/api-docs/go.mod
@@ -0,0 +1,18 @@
+module github.com/docker/docs/hack/api-docs
+
+go 1.26.5
+
+require (
+ github.com/pb33f/libopenapi v0.38.7
+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
+)
+
+require (
+ github.com/bahlo/generic-list-go v0.2.0 // indirect
+ github.com/buger/jsonparser v1.1.2 // indirect
+ github.com/pb33f/jsonpath v0.8.2 // indirect
+ github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
+ go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
+ golang.org/x/sync v0.22.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+)
diff --git a/hack/api-docs/go.sum b/hack/api-docs/go.sum
new file mode 100644
index 000000000000..304e372c1e29
--- /dev/null
+++ b/hack/api-docs/go.sum
@@ -0,0 +1,30 @@
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
+github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
+github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/pb33f/jsonpath v0.8.2 h1:Ou4C7zjYClBm97dfZjDCjdZGusJoynv/vrtiEKNfj2Y=
+github.com/pb33f/jsonpath v0.8.2/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo=
+github.com/pb33f/libopenapi v0.38.7 h1:Q2jfgRPdnU38WW8wQvrX2HEPGiqsxj01PX1BHmAEihc=
+github.com/pb33f/libopenapi v0.38.7/go.mod h1:naZ03Auhn7i+RJtMv8ck8l7Ag8E2/x2w66j9vsDFL38=
+github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
+github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
+github.com/pb33f/testify v0.1.0 h1:g48/HDU/jn2COspS4nM0scptxiKTJ4DnbX/4ehK6IZ8=
+github.com/pb33f/testify v0.1.0/go.mod h1:nq283P/jJ8hXMmdhAqfj7BJIz0y+6IOHj9q0044rKt4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
+go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
+golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
+golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/hack/api-docs/main.go b/hack/api-docs/main.go
new file mode 100644
index 000000000000..26a809d93f0d
--- /dev/null
+++ b/hack/api-docs/main.go
@@ -0,0 +1,754 @@
+// api-docs prepares a source-preserving reference model for Docker API documentation.
+package main
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/url"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/pb33f/libopenapi"
+ "github.com/pb33f/libopenapi/datamodel"
+ js "github.com/santhosh-tekuri/jsonschema/v6"
+ yaml "go.yaml.in/yaml/v4"
+)
+
+type Object = map[string]any
+
+const dialect = "https://spec.openapis.org/oas/3.1/dialect/base"
+
+var methods = []string{"get", "put", "post", "delete", "options", "head", "patch", "trace", "query"}
+
+func obj(v any) Object {
+ m, _ := v.(map[string]any)
+ if m == nil {
+ return Object{}
+ }
+ return m
+}
+func arr(v any) []any { a, _ := v.([]any); return a }
+func str(v any) string { s, _ := v.(string); return s }
+func keys(m Object) []string {
+ a := make([]string, 0, len(m))
+ for k := range m {
+ a = append(a, k)
+ }
+ sort.Strings(a)
+ return a
+}
+func hash(b []byte) string { s := sha256.Sum256(b); return hex.EncodeToString(s[:]) }
+func encoded(v any) []byte {
+ b, e := json.MarshalIndent(v, "", " ")
+ if e != nil {
+ panic(e)
+ }
+ return append(b, '\n')
+}
+func esc(s string) string { return strings.ReplaceAll(strings.ReplaceAll(s, "~", "~0"), "/", "~1") }
+func pointer(root any, p string) (any, error) {
+ if p == "" {
+ return root, nil
+ }
+ if !strings.HasPrefix(p, "/") {
+ return nil, fmt.Errorf("unsupported pointer %s", p)
+ }
+ for _, k := range strings.Split(p[1:], "/") {
+ k = strings.ReplaceAll(strings.ReplaceAll(k, "~1", "/"), "~0", "~")
+ switch v := root.(type) {
+ case map[string]any:
+ var ok bool
+ root, ok = v[k]
+ if !ok {
+ return nil, fmt.Errorf("missing pointer %s", p)
+ }
+ case []any:
+ var n int
+ if _, e := fmt.Sscanf(k, "%d", &n); e != nil || n < 0 || n >= len(v) {
+ return nil, fmt.Errorf("invalid array pointer %s", p)
+ }
+ root = v[n]
+ default:
+ return nil, fmt.Errorf("non-container pointer %s", p)
+ }
+ }
+ return root, nil
+}
+func readJSON(p string) (any, error) {
+ b, e := os.ReadFile(p)
+ if e != nil {
+ return nil, e
+ }
+ return js.UnmarshalJSON(bytes.NewReader(b))
+}
+func writeJSON(p string, v any) error {
+ if e := os.MkdirAll(filepath.Dir(p), 0755); e != nil {
+ return e
+ }
+ return os.WriteFile(p, encoded(v), 0644)
+}
+
+type Diagnostic struct {
+ Rule string `json:"rule"`
+ Pointer string `json:"pointer"`
+ Message string `json:"message"`
+}
+type Source struct {
+ ID string `json:"id"`
+ Product string `json:"product"`
+ Title string `json:"title"`
+ Source string `json:"source"`
+ Owner string `json:"owner"`
+ Manual string `json:"manual"`
+ Connection string `json:"connection"`
+ Guides []string `json:"guides"`
+}
+type Registry struct{ resources map[string]any }
+
+func (r *Registry) Load(uri string) (any, error) {
+ if v, ok := r.resources[uri]; ok {
+ return v, nil
+ }
+ return nil, fmt.Errorf("unlocked resource: %s", uri)
+}
+
+type Document struct {
+ Root Object
+ URI string
+ Registry *Registry
+ Diagnostics []Diagnostic
+ Schemas map[string]any
+ Locations map[string]int
+ Compiler *js.Compiler
+ Compiled map[string]*js.Schema
+ Digest string
+ Source Source
+ SchemaCount int
+ ExampleCount int
+}
+
+func (d *Document) issue(rule, p, msg string) {
+ d.Diagnostics = append(d.Diagnostics, Diagnostic{Rule: rule, Pointer: p, Message: strings.ReplaceAll(msg, d.URI, "source:"+d.Source.ID)})
+}
+func walk(v any, p string, fn func(Object, string)) {
+ switch n := v.(type) {
+ case map[string]any:
+ fn(n, p)
+ for _, k := range keys(n) {
+ walk(n[k], p+"/"+esc(k), fn)
+ }
+ case []any:
+ for i, x := range n {
+ walk(x, fmt.Sprintf("%s/%d", p, i), fn)
+ }
+ }
+}
+func strictSource(file string) (Object, libopenapi.Document, error) {
+ b, e := os.ReadFile(file)
+ if e != nil {
+ return nil, nil, e
+ }
+ cfg := datamodel.NewDocumentConfiguration()
+ cfg.AllowRemoteReferences = false
+ cfg.AllowFileReferences = false
+ cfg.SkipExternalRefResolution = true
+ doc, e := libopenapi.NewDocumentWithConfiguration(b, cfg)
+ if e != nil {
+ return nil, nil, e
+ }
+ v, e := strictFragment(file)
+ return obj(v), doc, e
+}
+
+// YAML timestamp scalars are strings in the JSON data model. Decode source nodes
+// explicitly: libopenapi's JSON convenience view normalizes their spelling.
+func strictFragment(file string) (any, error) {
+ b, e := os.ReadFile(file)
+ if e != nil {
+ return nil, e
+ }
+ var node yaml.Node
+ if e = yaml.Unmarshal(b, &node); e != nil {
+ return nil, e
+ }
+ v, e := sourceValue(&node, map[*yaml.Node]bool{})
+ if e != nil {
+ return nil, e
+ }
+ raw, e := json.Marshal(v)
+ if e != nil {
+ return nil, e
+ }
+ return js.UnmarshalJSON(bytes.NewReader(raw))
+}
+func sourceValue(n *yaml.Node, active map[*yaml.Node]bool) (any, error) {
+ if active[n] {
+ return nil, errors.New("cyclic YAML alias")
+ }
+ active[n] = true
+ defer delete(active, n)
+ switch n.Kind {
+ case yaml.DocumentNode:
+ if len(n.Content) != 1 {
+ return nil, errors.New("expected one YAML document")
+ }
+ return sourceValue(n.Content[0], active)
+ case yaml.AliasNode:
+ return sourceValue(n.Alias, active)
+ case yaml.MappingNode:
+ m := Object{}
+ for i := 0; i < len(n.Content); i += 2 {
+ k := n.Content[i]
+ if k.Kind != yaml.ScalarNode || k.Value == "<<" {
+ return nil, errors.New("complex/merge YAML keys are outside the source profile")
+ }
+ if _, ok := m[k.Value]; ok {
+ return nil, fmt.Errorf("duplicate key %s at line %d", k.Value, k.Line)
+ }
+ v, e := sourceValue(n.Content[i+1], active)
+ if e != nil {
+ return nil, e
+ }
+ m[k.Value] = v
+ }
+ return m, nil
+ case yaml.SequenceNode:
+ a := []any{}
+ for _, c := range n.Content {
+ v, e := sourceValue(c, active)
+ if e != nil {
+ return nil, e
+ }
+ a = append(a, v)
+ }
+ return a, nil
+ case yaml.ScalarNode:
+ if n.Tag == "!!str" || n.Tag == "!!timestamp" {
+ return n.Value, nil
+ }
+ var v any
+ e := n.Decode(&v)
+ return v, e
+ }
+ return nil, errors.New("unsupported YAML node")
+}
+
+func loadDocument(file, metaDir string) (*Document, error) {
+ abs, _ := filepath.Abs(file)
+ root, parsed, e := strictSource(abs)
+ if e != nil {
+ return nil, e
+ }
+ uri := (&url.URL{Scheme: "file", Path: abs}).String()
+ b, _ := os.ReadFile(file)
+ d := &Document{Root: root, URI: uri, Diagnostics: []Diagnostic{}, Registry: &Registry{resources: map[string]any{uri: root}}, Schemas: map[string]any{}, Locations: map[string]int{}, Compiled: map[string]*js.Schema{}, Digest: hash(b)}
+ // libopenapi's typed model supplies operation views/locations; schema serialization is never used.
+ model, modelErr := parsed.BuildV3Model()
+ if modelErr == nil && model != nil && model.Model.Paths != nil {
+ for p, item := range model.Model.Paths.PathItems.FromOldest() {
+ for m, op := range item.GetOperations().FromOldest() {
+ if op.GoLow().RootNode != nil {
+ d.Locations["/paths/"+esc(p)+"/"+m] = op.GoLow().RootNode.Line
+ }
+ }
+ }
+ }
+ // All file resources are acquired before compilation, under a bounded source directory.
+ var acquire func(any, string) error
+ acquire = func(v any, base string) error {
+ var failure error
+ walk(v, "", func(n Object, _ string) {
+ for _, k := range []string{"$ref", "$dynamicRef"} {
+ ref, ok := n[k].(string)
+ if !ok {
+ return
+ }
+ u, er := url.Parse(ref)
+ if er != nil {
+ failure = er
+ return
+ }
+ bu, _ := url.Parse(base)
+ u = bu.ResolveReference(u)
+ u.Fragment = ""
+ id := u.String()
+ if _, ok := d.Registry.resources[id]; ok {
+ return
+ }
+ if u.Scheme != "file" {
+ failure = fmt.Errorf("unlocked reference %s", id)
+ return
+ }
+ rel, er := filepath.Rel(filepath.Dir(abs), u.Path)
+ if er != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
+ failure = fmt.Errorf("reference escapes package: %s", ref)
+ return
+ }
+ r, er := strictFragment(u.Path)
+ if er != nil {
+ failure = er
+ return
+ }
+ d.Registry.resources[id] = r
+ if er = acquire(r, id); er != nil {
+ failure = er
+ }
+ }
+ })
+ return failure
+ }
+ if e = acquire(root, uri); e != nil {
+ return nil, e
+ }
+ lockRaw, e := os.ReadFile(filepath.Join(metaDir, "lock.json"))
+ if e != nil {
+ return nil, e
+ }
+ var lock []struct{ File, URI, SHA256 string }
+ if e = json.Unmarshal(lockRaw, &lock); e != nil {
+ return nil, e
+ }
+ for _, l := range lock {
+ b, e := os.ReadFile(filepath.Join(metaDir, l.File))
+ if e != nil {
+ return nil, e
+ }
+ if hash(b) != l.SHA256 {
+ return nil, fmt.Errorf("dialect digest mismatch: %s", l.File)
+ }
+ v, e := js.UnmarshalJSON(bytes.NewReader(b))
+ if e != nil {
+ return nil, e
+ }
+ d.Registry.resources[l.URI] = v
+ if id := str(obj(v)["$id"]); id != "" {
+ d.Registry.resources[id] = v
+ }
+ }
+ d.Compiler = js.NewCompiler()
+ d.Compiler.DefaultDraft(js.Draft2020)
+ d.Compiler.UseLoader(d.Registry)
+ for id, v := range d.Registry.resources {
+ if e = d.Compiler.AddResource(id, v); e != nil {
+ return nil, e
+ }
+ }
+ return d, nil
+}
+func (d *Document) resolve(v any) any {
+ seen := map[string]bool{}
+ for {
+ r := str(obj(v)["$ref"])
+ if r == "" {
+ return v
+ }
+ if seen[r] {
+ return v
+ }
+ seen[r] = true
+ u, e := url.Parse(r)
+ if e != nil {
+ return v
+ }
+ base, _ := url.Parse(d.URI)
+ u = base.ResolveReference(u)
+ frag := u.Fragment
+ u.Fragment = ""
+ target, ok := d.Registry.resources[u.String()]
+ if !ok {
+ return v
+ }
+ resolved, e := pointer(target, frag)
+ if e != nil {
+ return v
+ }
+ merged := Object{}
+ for k, x := range obj(resolved) {
+ merged[k] = x
+ }
+ for _, k := range []string{"summary", "description"} {
+ if x, ok := obj(v)[k]; ok {
+ merged[k] = x
+ }
+ }
+ if len(merged) == 0 {
+ return resolved
+ }
+ v = merged
+ }
+}
+func (d *Document) schemaRoots(v any, p string) {
+ switch n := v.(type) {
+ case map[string]any:
+ for _, k := range keys(n) {
+ x := n[k]
+ q := p + "/" + esc(k)
+ if k == "schemas" && p == "/components" {
+ for name, s := range obj(x) {
+ d.Schemas[q+"/"+esc(name)] = s
+ }
+ } else if k == "schema" || k == "itemSchema" {
+ d.Schemas[q] = x
+ } else if !strings.HasPrefix(k, "x-") && k != "example" && k != "examples" && k != "value" {
+ d.schemaRoots(x, q)
+ }
+ }
+ case []any:
+ for i, x := range n {
+ d.schemaRoots(x, fmt.Sprintf("%s/%d", p, i))
+ }
+ }
+}
+func (d *Document) validate(metaDir string) {
+ if d.Root["openapi"] != "3.2.0" {
+ d.issue("S1", "/openapi", "Expected OpenAPI 3.2.0")
+ }
+ if d.Root["jsonSchemaDialect"] != dialect {
+ d.issue("S1", "/jsonSchemaDialect", "Expected the selected OAS dialect")
+ }
+ documentSchema, e := d.Compiler.Compile("https://spec.openapis.org/oas/3.2/schema/2025-09-17")
+ if e != nil {
+ d.issue("structure", "", e.Error())
+ } else if e = documentSchema.Validate(d.Root); e != nil {
+ d.issue("structure", "", e.Error())
+ }
+ // OAS Reference Objects also need explicit validation: a document schema cannot prove their target exists.
+ walk(d.Root, "", func(n Object, p string) {
+ if r := str(n["$ref"]); r != "" {
+ u, er := url.Parse(r)
+ if er != nil {
+ d.issue("reference", p, er.Error())
+ return
+ }
+ base, _ := url.Parse(d.URI)
+ u = base.ResolveReference(u)
+ frag := u.Fragment
+ u.Fragment = ""
+ v, ok := d.Registry.resources[u.String()]
+ if !ok {
+ d.issue("reference", p, "Unregistered resource "+r)
+ } else if strings.HasPrefix(frag, "/") {
+ if _, er = pointer(v, frag); er != nil {
+ d.issue("reference", p, er.Error())
+ }
+ }
+ }
+ })
+ if len(obj(d.Root["webhooks"])) > 0 {
+ d.issue("capability", "/webhooks", "Webhook navigation is not supported by the reference renderer")
+ }
+ walk(d.Root, "", func(n Object, p string) {
+ if len(obj(n["callbacks"])) > 0 {
+ d.issue("capability", p+"/callbacks", "Callback navigation is not supported by the reference renderer")
+ }
+ })
+ d.schemaRoots(d.Root, "")
+ schemaMeta, e := d.Compiler.Compile(dialect)
+ if e != nil {
+ d.issue("dialect", "", e.Error())
+ return
+ }
+ for _, p := range keys(d.Schemas) {
+ s := d.Schemas[p]
+ d.SchemaCount++
+ if e = schemaMeta.Validate(s); e != nil {
+ d.issue("schema", p, e.Error())
+ continue
+ }
+ compiled, e := d.Compiler.Compile(d.URI + "#" + p)
+ if e != nil {
+ d.issue("schema", p, e.Error())
+ continue
+ }
+ d.Compiled[p] = compiled
+ d.schemaExamples(s, p)
+ }
+ // Media, parameter and header examples are separate from examples nested inside schemas.
+ walk(d.Root, "", func(n Object, p string) {
+ schemaKey := "schema"
+ if _, ok := n["itemSchema"]; ok {
+ schemaKey = "itemSchema"
+ }
+ c := d.Compiled[p+"/"+schemaKey]
+ if c == nil {
+ return
+ }
+ if v, ok := n["example"]; ok {
+ d.example(c, v, p+"/example")
+ }
+ for _, name := range keys(obj(n["examples"])) {
+ ex := obj(d.resolve(obj(n["examples"])[name]))
+ if v, ok := ex["value"]; ok {
+ d.example(c, v, p+"/examples/"+esc(name)+"/value")
+ }
+ if _, ok := ex["externalValue"]; ok {
+ d.issue("example-external", p+"/examples/"+esc(name), "External example requires a locked media fixture")
+ }
+ }
+ })
+ ids := map[string]bool{}
+ tags := map[string]string{}
+ for _, raw := range arr(d.Root["tags"]) {
+ t := obj(raw)
+ tags[str(t["name"])] = str(t["kind"])
+ }
+ for _, op := range d.operations() {
+ id := str(op["id"])
+ p := str(op["pointer"])
+ if id == "" || ids[id] {
+ d.issue("S4", p, "Operation ID must be present and unique")
+ }
+ ids[id] = true
+ opTags := arr(op["tags"])
+ if len(opTags) == 0 || tags[str(opTags[0])] != "nav" {
+ d.issue("S5", p+"/tags", "The first operation tag must identify a declared navigation group")
+ }
+ for _, tag := range opTags {
+ if _, ok := tags[str(tag)]; !ok {
+ d.issue("S5", p+"/tags", "Operation tag is undeclared: "+str(tag))
+ }
+ }
+ if len(arr(op["servers"])) == 0 && d.Source.Connection != "unix" {
+ d.issue("S6", p, "Operation needs effective servers or a local connection profile")
+ }
+ if strings.TrimSpace(str(op["description"])) == "" {
+ d.issue("S4", p+"/description", "Operation description required")
+ }
+ for _, pr := range arr(op["parameters"]) {
+ param := obj(pr)
+ if param["in"] == "header" {
+ switch strings.ToLower(str(param["name"])) {
+ case "authorization", "accept", "content-type":
+ d.issue("S8", str(param["pointer"]), "OpenAPI ignores this header parameter; use security or media types")
+ }
+ }
+ if str(param["description"]) == "" {
+ d.issue("S8", str(param["pointer"])+"/description", "Parameter description requires editorial review")
+ }
+ }
+ for _, variant := range arr(op["variants"]) {
+ v := obj(variant)
+ if str(v["media"]) != "" && len(arr(v["examples"])) == 0 {
+ d.issue("S11", str(v["pointer"]), "Media variant needs a reviewed example or transfer fixture")
+ }
+ }
+ }
+}
+func (d *Document) schemaExamples(s any, p string) {
+ n := obj(s)
+ if len(n) == 0 {
+ return
+ }
+ if _, ok := n["$schema"]; ok && n["$schema"] != dialect {
+ d.issue("S1", p+"/$schema", "Schema dialect override is outside the profile")
+ }
+ c, e := d.Compiler.Compile(d.URI + "#" + p)
+ if e == nil {
+ if v, ok := n["example"]; ok {
+ d.example(c, v, p+"/example")
+ }
+ for i, v := range arr(n["examples"]) {
+ d.example(c, v, fmt.Sprintf("%s/examples/%d", p, i))
+ }
+ }
+ for k, x := range n {
+ switch k {
+ case "properties", "patternProperties", "$defs", "dependentSchemas":
+ for _, name := range keys(obj(x)) {
+ d.schemaExamples(obj(x)[name], p+"/"+k+"/"+esc(name))
+ }
+ case "allOf", "anyOf", "oneOf", "prefixItems":
+ for i, v := range arr(x) {
+ d.schemaExamples(v, fmt.Sprintf("%s/%s/%d", p, k, i))
+ }
+ case "items", "additionalProperties", "unevaluatedProperties", "contains", "not", "if", "then", "else", "contentSchema", "propertyNames":
+ d.schemaExamples(x, p+"/"+k)
+ }
+ }
+}
+func (d *Document) example(c *js.Schema, v any, p string) {
+ d.ExampleCount++
+ if e := c.Validate(v); e != nil {
+ d.issue("example", p, e.Error())
+ }
+}
+func (d *Document) effective(root, item, op Object, k string) any {
+ if v, ok := op[k]; ok {
+ return v
+ }
+ if k == "servers" {
+ if v, ok := item[k]; ok {
+ return v
+ }
+ }
+ return root[k]
+}
+func (d *Document) parameters(item, op Object, p string) []any {
+ a := []any{}
+ positions := map[string]int{}
+ for i, container := range []Object{item, op} {
+ for j, raw := range arr(container["parameters"]) {
+ v := obj(d.resolve(raw))
+ c := Object{}
+ for k, x := range v {
+ c[k] = x
+ }
+ prefix := p
+ if i == 0 {
+ prefix = p[:strings.LastIndex(p, "/")]
+ }
+ c["pointer"] = fmt.Sprintf("%s/parameters/%d", prefix, j)
+ key := str(v["in"]) + ":" + str(v["name"])
+ if n, ok := positions[key]; ok {
+ a[n] = c
+ } else {
+ positions[key] = len(a)
+ a = append(a, c)
+ }
+ }
+ }
+ return a
+}
+func exampleList(v Object) []any {
+ out := []any{}
+ if x, ok := v["example"]; ok {
+ out = append(out, Object{"name": "Example", "value": x})
+ }
+ for _, name := range keys(obj(v["examples"])) {
+ x := obj(obj(v["examples"])[name])
+ if value, ok := x["value"]; ok {
+ out = append(out, Object{"name": name, "value": value})
+ }
+ }
+ return out
+}
+func (d *Document) variants(op Object, p string) []any {
+ out := []any{}
+ add := func(direction, status string, raw any, ptr string) {
+ container := obj(d.resolve(raw))
+ content := obj(container["content"])
+ if len(content) == 0 {
+ out = append(out, Object{"direction": direction, "status": status, "description": container["description"], "headers": container["headers"], "media": "", "pointer": ptr, "examples": []any{}})
+ }
+ for _, media := range keys(content) {
+ v := obj(content[media])
+ a := Object{"direction": direction, "status": status, "description": container["description"], "headers": container["headers"], "media": media, "pointer": ptr + "/content/" + esc(media), "examples": d.mediaExamples(v), "required": container["required"]}
+ for _, k := range []string{"schema", "itemSchema", "encoding"} {
+ if s, ok := v[k]; ok {
+ a[k] = s
+ }
+ }
+ out = append(out, a)
+ }
+ }
+ if v, ok := op["requestBody"]; ok {
+ add("Request", "", v, p+"/requestBody")
+ }
+ for _, code := range keys(obj(op["responses"])) {
+ add("Response", code, obj(op["responses"])[code], p+"/responses/"+esc(code))
+ }
+ return out
+}
+func (d *Document) operations() []Object {
+ out := []Object{}
+ for _, path := range keys(obj(d.Root["paths"])) {
+ item := obj(d.resolve(obj(d.Root["paths"])[path]))
+ ms := append([]string{}, methods...)
+ for m := range obj(item["additionalOperations"]) {
+ ms = append(ms, m)
+ }
+ for _, method := range ms {
+ raw, ok := item[method]
+ extra := !ok
+ if !ok {
+ raw, ok = obj(item["additionalOperations"])[method]
+ }
+ if !ok {
+ continue
+ }
+ op := obj(raw)
+ p := "/paths/" + esc(path) + "/" + method
+ if extra {
+ p = "/paths/" + esc(path) + "/additionalOperations/" + esc(method)
+ }
+ v := Object{"id": op["operationId"], "method": strings.ToUpper(method), "path": path, "pointer": p, "line": d.Locations[p], "summary": op["summary"], "description": op["description"], "tags": op["tags"], "deprecated": op["deprecated"], "servers": d.effective(d.Root, item, op, "servers"), "security": d.effective(d.Root, item, op, "security"), "parameters": d.parameters(item, op, p), "variants": d.variants(op, p), "raw": op}
+ out = append(out, v)
+ }
+ }
+ return out
+}
+func main() {
+ if e := run(); e != nil {
+ fmt.Fprintln(os.Stderr, e)
+ os.Exit(1)
+ }
+}
+func run() error {
+ if len(os.Args) < 3 {
+ return errors.New("usage: api-docs check|generate|inspect|sources ROOT")
+ }
+ command := os.Args[1]
+ if command != "check" && command != "generate" && command != "inspect" && command != "sources" {
+ return fmt.Errorf("unknown command: %s", command)
+ }
+ if len(os.Args) > 3 {
+ return errors.New("unexpected arguments")
+ }
+ root, _ := filepath.Abs(os.Args[2])
+ dir := filepath.Join(root, "hack/api-docs")
+ meta := filepath.Join(dir, "validation", "dialects")
+ manifestRaw, e := os.ReadFile(filepath.Join(dir, "sources.json"))
+ if e != nil {
+ return e
+ }
+ var manifest struct {
+ APIs []Source `json:"apis"`
+ LegacyAPIs []Object `json:"legacyAPIs"`
+ }
+ if e = json.Unmarshal(manifestRaw, &manifest); e != nil {
+ return e
+ }
+ if command == "sources" {
+ for _, src := range manifest.APIs {
+ fmt.Printf("%s\t%s\n", src.ID, src.Source)
+ }
+ return nil
+ }
+ reports := []any{}
+ models := []any{}
+ blocking := 0
+ for _, src := range manifest.APIs {
+ d, e := loadDocument(filepath.Join(root, src.Source), meta)
+ if e != nil {
+ return fmt.Errorf("%s: %w", src.ID, e)
+ }
+ d.Source = src
+ d.validate(meta)
+ blocking += len(d.Diagnostics)
+ reports = append(reports, Object{"api": src.ID, "sha256": d.Digest, "schemas": d.SchemaCount, "examples": d.ExampleCount, "operations": len(d.operations()), "diagnostics": d.Diagnostics})
+ models = append(models, d.model())
+ fmt.Printf("%s: %d operations, %d schema roots, %d examples, %d diagnostics\n", src.ID, len(d.operations()), d.SchemaCount, d.ExampleCount, len(d.Diagnostics))
+ }
+ out := filepath.Join(root, "tmp/api-reference")
+ if e = writeJSON(filepath.Join(out, "validation.json"), reports); e != nil {
+ return e
+ }
+ if command == "inspect" {
+ return nil
+ }
+ if blocking > 0 {
+ return fmt.Errorf("%d blocking diagnostics; see tmp/api-reference/validation.json", blocking)
+ }
+ if command == "generate" {
+ if e = writeJSON(filepath.Join(out, "data/api-reference.json"), Object{"modelVersion": 1, "apis": models, "legacyAPIs": manifest.LegacyAPIs}); e != nil {
+ return e
+ }
+ }
+ return nil
+}
diff --git a/hack/api-docs/main_test.go b/hack/api-docs/main_test.go
new file mode 100644
index 000000000000..a905bc8dfbac
--- /dev/null
+++ b/hack/api-docs/main_test.go
@@ -0,0 +1,285 @@
+package main
+
+import (
+ js "github.com/santhosh-tekuri/jsonschema/v6"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func fixture(t *testing.T, name string) *Document {
+ t.Helper()
+ d, e := loadDocument(filepath.Join("testdata", name), "validation/dialects")
+ if e != nil {
+ t.Fatal(e)
+ }
+ d.Source = Source{ID: "test"}
+ return d
+}
+func TestActualDialectAndReferences(t *testing.T) {
+ d := fixture(t, "valid.yaml")
+ d.validate("")
+ for _, x := range d.Diagnostics {
+ t.Errorf("%s: %s", x.Rule, x.Message)
+ }
+ if len(d.operations()) != 4 {
+ t.Fatal("operation omitted")
+ }
+ var query Object
+ for _, op := range d.operations() {
+ if op["method"] == "QUERY" {
+ query = op
+ }
+ }
+ if query == nil {
+ t.Fatal("QUERY omitted")
+ }
+ if obj(arr(query["servers"])[0])["url"] != "https://search.example.test" {
+ t.Fatal("path server override lost")
+ }
+ p := obj(arr(query["parameters"])[0])
+ if p["example"] != jsonNumber("0") {
+ t.Fatalf("parameter override/zero lost: %#v", p["example"])
+ }
+ if len(arr(query["parameters"])) != 1 {
+ t.Fatal("duplicate overridden parameter")
+ }
+}
+func jsonNumber(s string) any {
+ v, e := js.UnmarshalJSON(strings.NewReader(s))
+ if e != nil {
+ panic(e)
+ }
+ return v
+}
+func TestNegativeFixtures(t *testing.T) {
+ for _, tc := range []struct{ name, rule string }{{"invalid-info.yaml", "structure"}, {"invalid-item-type.yaml", "schema"}, {"invalid-example.yaml", "example"}, {"profile-missing-id.yaml", "S4"}} {
+ t.Run(tc.name, func(t *testing.T) {
+ d := fixture(t, tc.name)
+ d.validate("")
+ found := false
+ for _, x := range d.Diagnostics {
+ found = found || x.Rule == tc.rule
+ }
+ if !found {
+ t.Fatalf("missing %s diagnostic: %+v", tc.rule, d.Diagnostics)
+ }
+ })
+ }
+ for _, name := range []string{"duplicate-key.yaml", "invalid-stream-ref.yaml"} {
+ t.Run(name, func(t *testing.T) {
+ d, e := loadDocument(filepath.Join("testdata", name), "validation/dialects")
+ if e == nil {
+ d.validate("")
+ for _, x := range d.Diagnostics {
+ if x.Rule == "reference" || x.Rule == "schema" {
+ return
+ }
+ }
+ t.Fatal("invalid source accepted")
+ }
+ })
+ }
+}
+func TestSchemaSemantics(t *testing.T) {
+ d := fixture(t, "valid.yaml")
+ c := d.Compiler
+ cases := []struct {
+ name string
+ schema any
+ valid, invalid any
+ }{{"boolean false", false, nil, "x"}, {"ref siblings", Object{"$defs": Object{"base": Object{"type": "object", "required": []any{"a"}}}, "$ref": "#/$defs/base", "required": []any{"b"}}, Object{"a": true, "b": true}, Object{"b": true}}, {"recursive", Object{"type": "object", "properties": Object{"next": Object{"$ref": "#"}}}, Object{"next": Object{}}, Object{"next": 1}}, {"anchor", Object{"$defs": Object{"x": Object{"$anchor": "x", "type": "boolean"}}, "$ref": "#x"}, false, "false"}, {"dynamic", Object{"$dynamicAnchor": "node", "type": "object", "properties": Object{"child": Object{"$dynamicRef": "#node"}}}, Object{"child": Object{}}, Object{"child": false}}}
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ uri := "https://fixtures.test/" + strings.ReplaceAll(tc.name, " ", "-")
+ s := tc.schema
+ if m, ok := s.(map[string]any); ok {
+ m["$schema"] = dialect
+ }
+ if e := c.AddResource(uri, s); e != nil {
+ t.Fatal(e)
+ }
+ compiled, e := c.Compile(uri)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if tc.valid != nil {
+ if e = compiled.Validate(tc.valid); e != nil {
+ t.Fatal(e)
+ }
+ }
+ if e = compiled.Validate(tc.invalid); e == nil {
+ t.Fatal("invalid example accepted")
+ }
+ })
+ }
+}
+func TestAuthAndCurl(t *testing.T) {
+ schemes := Object{"bearer": Object{"type": "http", "scheme": "bearer"}, "key": Object{"type": "apiKey", "in": "header", "name": "X-Key"}}
+ op := Object{"method": "GET", "path": "/flag/{id}", "servers": []any{Object{"url": "https://api.test"}}, "securitySchemes": schemes, "security": []any{Object{"bearer": []any{}, "key": []any{}}, Object{}}, "parameters": []any{Object{"name": "id", "in": "path", "required": true, "example": "a/b"}, Object{"name": "enabled", "in": "query", "example": false}, Object{"name": "limit", "in": "query", "example": 0}}}
+ curl, _ := curlExample(Source{}, op)
+ for _, want := range []string{"a%2Fb", "enabled=false", "limit=0", "${TOKEN}", "X-Key: ${API_KEY}"} {
+ if !strings.Contains(curl, want) {
+ t.Errorf("missing %s: %s", want, curl)
+ }
+ }
+ op["security"] = []any{}
+ curl, _ = curlExample(Source{}, op)
+ if strings.Contains(curl, "Authorization") {
+ t.Fatal("anonymous operation gained auth")
+ }
+ op["method"] = "HEAD"
+ curl, _ = curlExample(Source{Connection: "unix", ID: "engine-1.56"}, op)
+ if !strings.Contains(curl, "--head") || !strings.Contains(curl, "--unix-socket") {
+ t.Fatal(curl)
+ }
+ d := &Document{}
+ root := Object{"security": []any{Object{"bearer": []any{}}}}
+ if !reflect.DeepEqual(d.effective(root, Object{}, Object{"security": []any{}}, "security"), []any{}) {
+ t.Fatal("empty override lost")
+ }
+}
+func TestFalseSchemaAndExamplePreserved(t *testing.T) {
+ b, e := os.ReadFile("testdata/valid.yaml")
+ if e != nil {
+ t.Fatal(e)
+ }
+ tmp := t.TempDir()
+ if e = os.WriteFile(filepath.Join(tmp, "spec.yaml"), []byte(strings.ReplaceAll(string(b), "schema: true", "schema: false")), 0600); e != nil {
+ t.Fatal(e)
+ }
+ if e = os.Mkdir(filepath.Join(tmp, "schemas"), 0700); e != nil {
+ t.Fatal(e)
+ }
+ event, _ := os.ReadFile("testdata/schemas/event.yaml")
+ os.WriteFile(filepath.Join(tmp, "schemas/event.yaml"), event, 0600)
+ d, e := loadDocument(filepath.Join(tmp, "spec.yaml"), "validation/dialects")
+ if e != nil {
+ t.Fatal(e)
+ }
+ d.validate("")
+ v, e := pointer(d.Root, "/paths/~1search/query/responses/200/content/application~1json/schema")
+ if e != nil || v != false {
+ t.Fatal("false schema changed")
+ }
+ for _, op := range d.operations() {
+ if op["path"] == "/public" {
+ ex := obj(arr(obj(arr(op["variants"])[0])["examples"])[0])
+ if ex["value"] != false {
+ t.Fatal("false example omitted")
+ }
+ }
+ }
+}
+
+func TestMediaExamplesFollowReferenceAnnotations(t *testing.T) {
+ d := &Document{URI: "https://example.test/api.yaml", Registry: &Registry{resources: map[string]any{
+ "https://example.test/api.yaml": Object{
+ "alias": Object{"$ref": "#/base", "examples": []any{false, jsonNumber("0")}},
+ "base": Object{"example": "base example"},
+ "cycle": Object{"$ref": "#/cycle"},
+ },
+ "https://example.test/schemas/alias.yaml": Object{"$ref": "base.yaml"},
+ "https://example.test/schemas/base.yaml": Object{"example": "external example"},
+ }}}
+ for _, tc := range []struct {
+ name string
+ media Object
+ values []any
+ }{
+ {"intermediate annotations", Object{"schema": Object{"$ref": "#/alias"}}, []any{false, jsonNumber("0")}},
+ {"nearest schema annotation", Object{"schema": Object{"$ref": "#/alias", "example": "local"}}, []any{"local"}},
+ {"media annotation", Object{"example": false, "schema": Object{"$ref": "#/alias"}}, []any{false}},
+ {"relative external reference", Object{"schema": Object{"$ref": "schemas/alias.yaml"}}, []any{"external example"}},
+ {"cycle", Object{"schema": Object{"$ref": "#/cycle"}}, []any{}},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ values := []any{}
+ for _, ex := range d.mediaExamples(tc.media) {
+ values = append(values, obj(ex)["value"])
+ }
+ if !reflect.DeepEqual(values, tc.values) {
+ t.Fatalf("examples = %#v, want %#v", values, tc.values)
+ }
+ })
+ }
+}
+
+func TestDVPResponseContracts(t *testing.T) {
+ d, err := loadDocument(filepath.Join("..", "..", "content", "reference", "api", "dvp", "latest.yaml"), "validation/dialects")
+ if err != nil {
+ t.Fatal(err)
+ }
+ d.Source = Source{ID: "dvp"}
+ d.validate("")
+ if len(d.Diagnostics) != 0 {
+ t.Fatalf("DVP must pass without exceptions: %+v", d.Diagnostics)
+ }
+ for _, tc := range []struct {
+ schema string
+ value string
+ valid bool
+ }{
+ {"TimespanModel", `{"month":7}`, true},
+ {"TimespanModel", `{"week":31}`, true},
+ {"TimespanModel", `{}`, false},
+ {"TimespanModel", `{"month":7,"week":31}`, false},
+ {"TimespanModel", `{"month":{"month":7}}`, false},
+ {"TimespanModel", `7`, false},
+ {"TimespanData", `{"months":[{"month":5},{"month":7}]}`, true},
+ {"TimespanData", `{"weeks":[]}`, true},
+ {"TimespanData", `{"months":[],"weeks":[]}`, false},
+ {"TimespanData", `{"months":[{}]}`, false},
+ {"TimespanData", `{"weeks":null}`, false},
+ {"NamespaceMetadata", `{"namespace":"org1","extraRepos":null,"extensionPublisher":false}`, true},
+ {"PullData", `{"pulls":null}`, true},
+ {"PullData", `{"pulls":[{"start":"2022-08-01T00:00:00Z","pullCount":0}]}`, true},
+ } {
+ t.Run(tc.schema+"/"+tc.value, func(t *testing.T) {
+ s, err := d.Compiler.Compile(d.URI + "#/components/schemas/" + tc.schema)
+ if err != nil {
+ t.Fatal(err)
+ }
+ value, err := js.UnmarshalJSON(strings.NewReader(tc.value))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := s.Validate(value); (err == nil) != tc.valid {
+ t.Fatalf("valid = %t, want %t: %v", err == nil, tc.valid, err)
+ }
+ })
+ }
+}
+
+func TestTimestampStringsRetainSpelling(t *testing.T) {
+ p := filepath.Join(t.TempDir(), "schema.yaml")
+ if e := os.WriteFile(p, []byte("type: string\nexample: 2021-01-05T21:06:53.506400Z\n"), 0600); e != nil {
+ t.Fatal(e)
+ }
+ v, e := strictFragment(p)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if obj(v)["example"] != "2021-01-05T21:06:53.506400Z" {
+ t.Fatalf("timestamp spelling changed: %v", v)
+ }
+}
+func TestCaseSensitiveSchemaRoutes(t *testing.T) {
+ if slug("Error") == slug("error") {
+ t.Fatal("schema route collision")
+ }
+}
+func TestUnlockedResourcesAndUnknownDialect(t *testing.T) {
+ d := fixture(t, "valid.yaml")
+ if _, e := d.Compiler.Compile("https://unlocked.test/schema"); e == nil {
+ t.Fatal("unlocked schema fetched")
+ }
+ if e := d.Compiler.AddResource("https://fixtures.test/custom", Object{"$schema": "https://unlocked.test/dialect", "type": "object"}); e != nil {
+ t.Fatal(e)
+ }
+ if _, e := d.Compiler.Compile("https://fixtures.test/custom"); e == nil {
+ t.Fatal("unknown dialect accepted")
+ }
+}
diff --git a/hack/api-docs/model.go b/hack/api-docs/model.go
new file mode 100644
index 000000000000..b7d519e77991
--- /dev/null
+++ b/hack/api-docs/model.go
@@ -0,0 +1,293 @@
+package main
+
+import (
+ "net/url"
+ "sort"
+ "strings"
+)
+
+func route(id string) string {
+ return "/reference/api/" + id + "/latest/"
+}
+func slug(s string) string { return url.PathEscape(s) }
+func (d *Document) model() Object {
+ ops := d.operations()
+ schemas := []any{}
+ schemaURLs := Object{}
+ for _, name := range keys(obj(obj(d.Root["components"])["schemas"])) {
+ p := "/components/schemas/" + esc(name)
+ u := route(d.Source.ID) + "schemas/" + slug(name) + "/"
+ schemaURLs["#"+p] = u
+ schemas = append(schemas, Object{"name": name, "pointer": p, "url": u, "schema": obj(obj(d.Root["components"])["schemas"])[name]})
+ }
+ for _, op := range ops {
+ for _, raw := range arr(op["variants"]) {
+ v := obj(raw)
+ for _, rawEx := range arr(v["examples"]) {
+ ex := obj(rawEx)
+ ex["text"], ex["language"] = exampleText(str(v["media"]), ex["value"])
+ }
+ }
+ }
+ for _, op := range ops {
+ op["url"] = route(d.Source.ID) + "operations/" + slug(str(op["id"])) + "/"
+ op["securitySchemes"] = obj(d.Root["components"])["securitySchemes"]
+ op["acceptMedia"] = responseMedia(op)
+ // HEAD negotiates the GET representation but has no response body.
+ if op["method"] == "HEAD" && op["acceptMedia"] == "" {
+ for _, get := range ops {
+ if get["method"] == "GET" && get["path"] == op["path"] {
+ op["acceptMedia"] = responseMedia(get)
+ break
+ }
+ }
+ }
+ op["curl"], op["curlNotes"] = curlExample(d.Source, op)
+ op["references"] = refs(op["raw"], schemaURLs)
+ op["requestSchema"] = firstRequestSchema(op)
+ }
+ return Object{"id": d.Source.ID, "product": d.Source.Product, "title": d.Source.Title, "version": obj(d.Root["info"])["version"], "description": obj(d.Root["info"])["description"], "url": route(d.Source.ID), "manual": d.Source.Manual, "guides": d.Source.Guides, "connection": d.Source.Connection, "servers": d.Root["servers"], "securitySchemes": obj(d.Root["components"])["securitySchemes"], "tags": d.Root["tags"], "operations": ops, "schemas": schemas, "schemaURLs": schemaURLs, "digest": d.Digest, "owner": d.Source.Owner, "source": d.Source.Source, "sourceURL": "/" + strings.TrimPrefix(d.Source.Source, "content/"), "diagnostics": d.Diagnostics, "schemaCount": d.SchemaCount, "exampleCount": d.ExampleCount}
+}
+func refs(v any, urls Object) []any {
+ found := map[string]bool{}
+ walk(v, "", func(n Object, _ string) {
+ if s := str(n["$ref"]); s != "" {
+ found[s] = true
+ }
+ })
+ out := []any{}
+ names := []string{}
+ for n := range found {
+ names = append(names, n)
+ }
+ sort.Strings(names)
+ for _, n := range names {
+ out = append(out, Object{"ref": n, "url": urls[n]})
+ }
+ return out
+}
+func firstRequestSchema(op Object) any {
+ for _, v := range arr(op["variants"]) {
+ m := obj(v)
+ if m["direction"] == "Request" {
+ return m["schema"]
+ }
+ }
+ return nil
+}
+func shell(s string) string { return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" }
+func scalar(v any) string {
+ if s, ok := v.(string); ok {
+ return s
+ }
+ return strings.TrimSpace(string(encoded(v)))
+}
+func parameterValue(p Object) (any, bool) {
+ if v, ok := p["example"]; ok {
+ return v, true
+ }
+ s := obj(p["schema"])
+ if v, ok := s["example"]; ok {
+ return v, true
+ }
+ if a := arr(s["examples"]); len(a) > 0 {
+ return a[0], true
+ }
+ return nil, false
+}
+
+func exampleText(media string, value any) (string, string) {
+ if s, ok := value.(string); ok && !strings.Contains(media, "json") {
+ return s, "text"
+ }
+ return strings.TrimSpace(string(encoded(value))), "json"
+}
+
+func responseMedia(op Object) string {
+ for _, raw := range arr(op["variants"]) {
+ v := obj(raw)
+ if v["direction"] == "Response" && strings.HasPrefix(str(v["status"]), "2") && str(v["media"]) != "" {
+ return str(v["media"])
+ }
+ }
+ return ""
+}
+
+func curlExample(src Source, op Object) (string, []string) {
+ notes := []string{}
+ args := []string{"curl"}
+ if op["method"] == "HEAD" {
+ args = append(args, "--head")
+ } else {
+ args = append(args, "--request "+str(op["method"]))
+ }
+ server := ""
+ if a := arr(op["servers"]); len(a) > 0 {
+ m := obj(a[0])
+ server = str(m["url"])
+ for name, v := range obj(m["variables"]) {
+ server = strings.ReplaceAll(server, "{"+name+"}", str(obj(v)["default"]))
+ }
+ }
+ if src.Connection == "unix" {
+ args = append(args, "--unix-socket \"${DOCKER_SOCKET:-/var/run/docker.sock}\"")
+ if !strings.HasPrefix(server, "/") {
+ server = "/v" + strings.TrimPrefix(src.ID, "engine-")
+ }
+ server = "http://localhost" + server
+ }
+ if server == "" {
+ server = "https://"
+ notes = append(notes, "Set the API server address.")
+ }
+ target := strings.TrimRight(server, "/") + str(op["path"])
+ query := []string{}
+ for _, raw := range arr(op["parameters"]) {
+ p := obj(raw)
+ name := str(p["name"])
+ v, has := parameterValue(p)
+ required, _ := p["required"].(bool)
+ if !has && !required {
+ continue
+ }
+ value := "<" + strings.ToUpper(name) + ">"
+ if has {
+ value = scalar(v)
+ }
+ location := str(p["in"])
+ if location == "path" {
+ part := value
+ if has {
+ part = url.PathEscape(value)
+ }
+ target = strings.ReplaceAll(target, "{"+name+"}", part)
+ } else if location == "header" {
+ args = append(args, "--header "+shell(name+": "+value))
+ } else if location == "query" {
+ style := str(p["style"])
+ if style == "" {
+ style = "form"
+ }
+ explode := true
+ if x, ok := p["explode"].(bool); ok {
+ explode = x
+ }
+ if list, ok := v.([]any); ok && style == "form" {
+ parts := []string{}
+ for _, x := range list {
+ parts = append(parts, scalar(x))
+ }
+ if explode {
+ for _, x := range parts {
+ query = append(query, url.QueryEscape(name)+"="+url.QueryEscape(x))
+ }
+ } else {
+ query = append(query, url.QueryEscape(name)+"="+url.QueryEscape(strings.Join(parts, ",")))
+ }
+ } else if len(obj(v)) > 0 || style != "form" {
+ notes = append(notes, "Serialize "+name+" using its documented "+style+" rules; this parameter is not generated.")
+ } else {
+ query = append(query, url.QueryEscape(name)+"="+url.QueryEscape(value))
+ }
+ } else {
+ notes = append(notes, "Supply "+name+" using its documented "+location+" encoding.")
+ }
+ }
+ if len(query) > 0 {
+ target += "?" + strings.Join(query, "&")
+ }
+ security := arr(op["security"])
+ if len(security) > 0 {
+ first := obj(security[0])
+ for _, scheme := range keys(first) {
+ definition := obj(obj(op["securitySchemes"])[scheme])
+ switch {
+ case definition["type"] == "http" && definition["scheme"] == "bearer":
+ token := "TOKEN"
+ if scheme == "scimToken" {
+ token = "SCIM_TOKEN"
+ }
+ if scheme == "registryToken" {
+ token = "REGISTRY_TOKEN"
+ }
+ args = append(args, "--header \"Authorization: Bearer ${"+token+"}\"")
+ case definition["type"] == "apiKey" && definition["in"] == "header":
+ args = append(args, "--header \""+str(definition["name"])+": ${API_KEY}\"")
+ default:
+ notes = append(notes, "Configure authentication scheme "+scheme+" using its documented transport or credential format.")
+ }
+ }
+ if len(security) > 1 {
+ notes = append(notes, "This example uses the first authentication alternative. Review the complete requirements.")
+ }
+ }
+ if media := str(op["acceptMedia"]); media != "" {
+ args = append(args, "--header "+shell("Accept: "+media))
+ }
+ for _, raw := range arr(op["variants"]) {
+ v := obj(raw)
+ if v["direction"] != "Request" {
+ continue
+ }
+ media := str(v["media"])
+ if media == "" {
+ continue
+ }
+ args = append(args, "--header "+shell("Content-Type: "+media))
+ examples := arr(v["examples"])
+ if len(examples) > 0 && strings.Contains(media, "json") {
+ args = append(args, "--data-raw "+shell(strings.TrimSpace(string(encoded(obj(examples[0])["value"])))))
+ } else {
+ args = append(args, "--data-binary @request-body")
+ notes = append(notes, "Prepare request-body using the selected media type and schema.")
+ }
+ break
+ }
+ args = append(args, shell(target))
+ return strings.Join(args, " \\\n "), notes
+}
+
+// Examples are annotations: retrieve them without flattening schema constraints.
+func (d *Document) mediaExamples(v Object) []any {
+ out := exampleList(v)
+ if len(out) > 0 {
+ return out
+ }
+ s := v["schema"]
+ seen := map[string]bool{}
+ base, _ := url.Parse(d.URI)
+ for s != nil {
+ n := obj(s)
+ if x, ok := n["example"]; ok {
+ return []any{Object{"name": "Schema example", "value": x}}
+ }
+ if xs := arr(n["examples"]); len(xs) > 0 {
+ for _, x := range xs {
+ out = append(out, Object{"name": "Schema example", "value": x})
+ }
+ return out
+ }
+ ref := str(n["$ref"])
+ if ref == "" {
+ break
+ }
+ u, err := url.Parse(ref)
+ if err != nil {
+ break
+ }
+ u = base.ResolveReference(u)
+ if seen[u.String()] {
+ break
+ }
+ seen[u.String()] = true
+ fragment := u.Fragment
+ u.Fragment = ""
+ // Follow one reference at a time so intermediate schema annotations survive.
+ s, err = pointer(d.Registry.resources[u.String()], fragment)
+ if err != nil {
+ break
+ }
+ base = u
+ }
+ return out
+}
diff --git a/hack/api-docs/run.sh b/hack/api-docs/run.sh
new file mode 100755
index 000000000000..e132d20253b0
--- /dev/null
+++ b/hack/api-docs/run.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+set -euo pipefail
+ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
+cd "$ROOT"
+BIN="$ROOT/tmp/api-reference/bin"
+mkdir -p "$BIN" "$ROOT/tmp/api-reference/reports"
+export GOWORK=off
+bootstrap() {
+ (cd hack/api-docs && go build -o "$BIN/api-docs" .)
+ if [[ ! -x "$BIN/vacuum-v0.30.3" ]]; then
+ GOBIN="$BIN" go install github.com/daveshanley/vacuum@v0.30.3
+ mv "$BIN/vacuum" "$BIN/vacuum-v0.30.3"
+ fi
+}
+policy() {
+ local sources
+ sources=$("$BIN/api-docs" sources "$ROOT")
+ while IFS=$'\t' read -r api source; do
+ "$BIN/vacuum-v0.30.3" lint --no-update-check --remote=false --ruleset hack/api-docs/validation/rules.yaml --fail-severity error --min-score 0 --no-banner --no-style --details "$source" > "tmp/api-reference/reports/$api-vacuum.txt" 2>&1 || {
+ cat "tmp/api-reference/reports/$api-vacuum.txt"; return 1;
+ }
+ done <<< "$sources"
+}
+generate() {
+ bootstrap
+ policy
+ "$BIN/api-docs" generate "$ROOT"
+}
+case "${1:-build}" in
+ bootstrap) bootstrap ;;
+ check) bootstrap; policy; "$BIN/api-docs" check "$ROOT" ;;
+ generate) generate ;;
+ test) (cd hack/api-docs && go test ./...) ;;
+ build|serve)
+ generate
+ hugo --destination tmp/api-reference/site --baseURL "${DOCS_URL:-http://localhost:1314}" --cleanDestinationDir
+ node hack/flatten-and-resolve.js tmp/api-reference/site
+ node hack/api-docs/verify-output.mjs tmp/api-reference/site
+ if [[ "${1:-build}" == serve ]]; then
+ exec python3 -m http.server "${DOCS_PORT:-1314}" --bind 127.0.0.1 --directory tmp/api-reference/site
+ fi
+ ;;
+ *) printf '%s\n' 'Usage: run.sh bootstrap|check|generate|test|build|serve' >&2; exit 2 ;;
+esac
diff --git a/hack/api-docs/sources.json b/hack/api-docs/sources.json
new file mode 100644
index 000000000000..d10f149e096d
--- /dev/null
+++ b/hack/api-docs/sources.json
@@ -0,0 +1,48 @@
+{
+ "apis": [
+ {
+ "id": "hub",
+ "product": "hub",
+ "title": "Docker Hub",
+ "source": "content/reference/api/hub/latest.yaml",
+ "owner": "docker/docs",
+ "manual": "/manuals/docker-hub/",
+ "connection": "hosted",
+ "guides": ["/manuals/security/provisioning/scim/provision-scim.md"]
+ },
+ {
+ "id": "dvp",
+ "product": "dvp",
+ "title": "Publisher analytics",
+ "source": "content/reference/api/dvp/latest.yaml",
+ "owner": "docker/docs",
+ "manual": "/manuals/docker-hub/repos/manage/trusted-content/insights-analytics.md",
+ "connection": "hosted",
+ "guides": []
+ },
+ {
+ "id": "registry",
+ "product": "registry",
+ "title": "Registry",
+ "source": "content/reference/api/registry/latest.yaml",
+ "owner": "docker/docs",
+ "manual": "/manuals/docker-hub/repos/",
+ "connection": "hosted",
+ "guides": ["/reference/api/registry/auth.md"]
+ }
+ ],
+ "legacyAPIs": [
+ {
+ "title": "AI Governance",
+ "url": "/reference/api/ai-governance/",
+ "connection": "hosted",
+ "description": "Organization policy and access controls."
+ },
+ {
+ "title": "Docker Engine",
+ "url": "/reference/api/engine/",
+ "connection": "unix",
+ "description": "Manage containers, images, networks, and volumes."
+ }
+ ]
+}
diff --git a/hack/api-docs/testdata/duplicate-key.yaml b/hack/api-docs/testdata/duplicate-key.yaml
new file mode 100644
index 000000000000..739c796ab82b
--- /dev/null
+++ b/hack/api-docs/testdata/duplicate-key.yaml
@@ -0,0 +1,102 @@
+openapi: 3.2.0
+jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
+info:
+ title: Docker documentation tooling fixture
+ version: '1'
+ version: '2'
+ description: Component capability fixture.
+servers:
+- url: https://api.example.test/v1
+security:
+- bearerAuth: []
+tags:
+- name: operations
+ summary: Operations
+ kind: nav
+ description: Fixture operations.
+paths:
+ /events:
+ get:
+ operationId: listEvents
+ tags:
+ - operations
+ summary: List events
+ responses:
+ '200':
+ description: Event stream
+ content:
+ text/event-stream:
+ itemSchema:
+ $ref: ./schemas/event.yaml
+ /search:
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 10
+ servers:
+ - url: https://search.example.test
+ query:
+ operationId: querySearch
+ tags:
+ - operations
+ summary: Search
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 0
+ responses:
+ '200':
+ description: Search result
+ content:
+ application/json:
+ schema: true
+ /public:
+ get:
+ operationId: getPublic
+ tags:
+ - operations
+ summary: Public result
+ security: []
+ responses:
+ '200':
+ description: A flag
+ content:
+ application/json:
+ schema:
+ type: boolean
+ example: false
+ /items:
+ get:
+ operationId: getItems
+ tags:
+ - operations
+ summary: Get items
+ responses:
+ '200':
+ description: Item
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Item'
+ description: Description beside a reference.
+components:
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ schemas:
+ Item:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ next:
+ anyOf:
+ - $ref: '#/components/schemas/Item'
+ - type: 'null'
diff --git a/hack/api-docs/testdata/invalid-example.yaml b/hack/api-docs/testdata/invalid-example.yaml
new file mode 100644
index 000000000000..37015deb1a44
--- /dev/null
+++ b/hack/api-docs/testdata/invalid-example.yaml
@@ -0,0 +1,101 @@
+openapi: 3.2.0
+jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
+info:
+ title: Docker documentation tooling fixture
+ version: '1'
+ description: Component capability fixture.
+servers:
+- url: https://api.example.test/v1
+security:
+- bearerAuth: []
+tags:
+- name: operations
+ summary: Operations
+ kind: nav
+ description: Fixture operations.
+paths:
+ /events:
+ get:
+ operationId: listEvents
+ tags:
+ - operations
+ summary: List events
+ responses:
+ '200':
+ description: Event stream
+ content:
+ text/event-stream:
+ itemSchema:
+ $ref: ./schemas/event.yaml
+ /search:
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 10
+ servers:
+ - url: https://search.example.test
+ query:
+ operationId: querySearch
+ tags:
+ - operations
+ summary: Search
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 0
+ responses:
+ '200':
+ description: Search result
+ content:
+ application/json:
+ schema: true
+ /public:
+ get:
+ operationId: getPublic
+ tags:
+ - operations
+ summary: Public result
+ security: []
+ responses:
+ '200':
+ description: A flag
+ content:
+ application/json:
+ schema:
+ type: boolean
+ example: not-a-boolean
+ /items:
+ get:
+ operationId: getItems
+ tags:
+ - operations
+ summary: Get items
+ responses:
+ '200':
+ description: Item
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Item'
+ description: Description beside a reference.
+components:
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ schemas:
+ Item:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ next:
+ anyOf:
+ - $ref: '#/components/schemas/Item'
+ - type: 'null'
diff --git a/hack/api-docs/testdata/invalid-info.yaml b/hack/api-docs/testdata/invalid-info.yaml
new file mode 100644
index 000000000000..3280debcaee6
--- /dev/null
+++ b/hack/api-docs/testdata/invalid-info.yaml
@@ -0,0 +1,100 @@
+openapi: 3.2.0
+jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
+info:
+ title: Docker documentation tooling fixture
+ description: Component capability fixture.
+servers:
+- url: https://api.example.test/v1
+security:
+- bearerAuth: []
+tags:
+- name: operations
+ summary: Operations
+ kind: nav
+ description: Fixture operations.
+paths:
+ /events:
+ get:
+ operationId: listEvents
+ tags:
+ - operations
+ summary: List events
+ responses:
+ '200':
+ description: Event stream
+ content:
+ text/event-stream:
+ itemSchema:
+ $ref: ./schemas/event.yaml
+ /search:
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 10
+ servers:
+ - url: https://search.example.test
+ query:
+ operationId: querySearch
+ tags:
+ - operations
+ summary: Search
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 0
+ responses:
+ '200':
+ description: Search result
+ content:
+ application/json:
+ schema: true
+ /public:
+ get:
+ operationId: getPublic
+ tags:
+ - operations
+ summary: Public result
+ security: []
+ responses:
+ '200':
+ description: A flag
+ content:
+ application/json:
+ schema:
+ type: boolean
+ example: false
+ /items:
+ get:
+ operationId: getItems
+ tags:
+ - operations
+ summary: Get items
+ responses:
+ '200':
+ description: Item
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Item'
+ description: Description beside a reference.
+components:
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ schemas:
+ Item:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ next:
+ anyOf:
+ - $ref: '#/components/schemas/Item'
+ - type: 'null'
diff --git a/hack/api-docs/testdata/invalid-item-type.yaml b/hack/api-docs/testdata/invalid-item-type.yaml
new file mode 100644
index 000000000000..524584656267
--- /dev/null
+++ b/hack/api-docs/testdata/invalid-item-type.yaml
@@ -0,0 +1,101 @@
+openapi: 3.2.0
+jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
+info:
+ title: Docker documentation tooling fixture
+ version: '1'
+ description: Component capability fixture.
+servers:
+- url: https://api.example.test/v1
+security:
+- bearerAuth: []
+tags:
+- name: operations
+ summary: Operations
+ kind: nav
+ description: Fixture operations.
+paths:
+ /events:
+ get:
+ operationId: listEvents
+ tags:
+ - operations
+ summary: List events
+ responses:
+ '200':
+ description: Event stream
+ content:
+ text/event-stream:
+ itemSchema:
+ type: definitely-invalid
+ /search:
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 10
+ servers:
+ - url: https://search.example.test
+ query:
+ operationId: querySearch
+ tags:
+ - operations
+ summary: Search
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 0
+ responses:
+ '200':
+ description: Search result
+ content:
+ application/json:
+ schema: true
+ /public:
+ get:
+ operationId: getPublic
+ tags:
+ - operations
+ summary: Public result
+ security: []
+ responses:
+ '200':
+ description: A flag
+ content:
+ application/json:
+ schema:
+ type: boolean
+ example: false
+ /items:
+ get:
+ operationId: getItems
+ tags:
+ - operations
+ summary: Get items
+ responses:
+ '200':
+ description: Item
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Item'
+ description: Description beside a reference.
+components:
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ schemas:
+ Item:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ next:
+ anyOf:
+ - $ref: '#/components/schemas/Item'
+ - type: 'null'
diff --git a/hack/api-docs/testdata/invalid-stream-ref.yaml b/hack/api-docs/testdata/invalid-stream-ref.yaml
new file mode 100644
index 000000000000..7d4edd2052bb
--- /dev/null
+++ b/hack/api-docs/testdata/invalid-stream-ref.yaml
@@ -0,0 +1,101 @@
+openapi: 3.2.0
+jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
+info:
+ title: Docker documentation tooling fixture
+ version: '1'
+ description: Component capability fixture.
+servers:
+- url: https://api.example.test/v1
+security:
+- bearerAuth: []
+tags:
+- name: operations
+ summary: Operations
+ kind: nav
+ description: Fixture operations.
+paths:
+ /events:
+ get:
+ operationId: listEvents
+ tags:
+ - operations
+ summary: List events
+ responses:
+ '200':
+ description: Event stream
+ content:
+ text/event-stream:
+ itemSchema:
+ $ref: ./schemas/missing.yaml
+ /search:
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 10
+ servers:
+ - url: https://search.example.test
+ query:
+ operationId: querySearch
+ tags:
+ - operations
+ summary: Search
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 0
+ responses:
+ '200':
+ description: Search result
+ content:
+ application/json:
+ schema: true
+ /public:
+ get:
+ operationId: getPublic
+ tags:
+ - operations
+ summary: Public result
+ security: []
+ responses:
+ '200':
+ description: A flag
+ content:
+ application/json:
+ schema:
+ type: boolean
+ example: false
+ /items:
+ get:
+ operationId: getItems
+ tags:
+ - operations
+ summary: Get items
+ responses:
+ '200':
+ description: Item
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Item'
+ description: Description beside a reference.
+components:
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ schemas:
+ Item:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ next:
+ anyOf:
+ - $ref: '#/components/schemas/Item'
+ - type: 'null'
diff --git a/hack/api-docs/testdata/profile-missing-id.yaml b/hack/api-docs/testdata/profile-missing-id.yaml
new file mode 100644
index 000000000000..99fba5ceccbd
--- /dev/null
+++ b/hack/api-docs/testdata/profile-missing-id.yaml
@@ -0,0 +1,100 @@
+openapi: 3.2.0
+jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
+info:
+ title: Docker documentation tooling fixture
+ version: '1'
+ description: Component capability fixture.
+servers:
+- url: https://api.example.test/v1
+security:
+- bearerAuth: []
+tags:
+- name: operations
+ summary: Operations
+ kind: nav
+ description: Fixture operations.
+paths:
+ /events:
+ get:
+ operationId: listEvents
+ tags:
+ - operations
+ summary: List events
+ responses:
+ '200':
+ description: Event stream
+ content:
+ text/event-stream:
+ itemSchema:
+ $ref: ./schemas/event.yaml
+ /search:
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 10
+ servers:
+ - url: https://search.example.test
+ query:
+ operationId: querySearch
+ tags:
+ - operations
+ summary: Search
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ example: 0
+ responses:
+ '200':
+ description: Search result
+ content:
+ application/json:
+ schema: true
+ /public:
+ get:
+ tags:
+ - operations
+ summary: Public result
+ security: []
+ responses:
+ '200':
+ description: A flag
+ content:
+ application/json:
+ schema:
+ type: boolean
+ example: false
+ /items:
+ get:
+ operationId: getItems
+ tags:
+ - operations
+ summary: Get items
+ responses:
+ '200':
+ description: Item
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Item'
+ description: Description beside a reference.
+components:
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ schemas:
+ Item:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ next:
+ anyOf:
+ - $ref: '#/components/schemas/Item'
+ - type: 'null'
diff --git a/hack/api-docs/testdata/schemas/event.yaml b/hack/api-docs/testdata/schemas/event.yaml
new file mode 100644
index 000000000000..b158529a5656
--- /dev/null
+++ b/hack/api-docs/testdata/schemas/event.yaml
@@ -0,0 +1,9 @@
+type: object
+required:
+- event
+- data
+properties:
+ event:
+ type: string
+ data:
+ type: string
diff --git a/hack/api-docs/testdata/valid.yaml b/hack/api-docs/testdata/valid.yaml
new file mode 100644
index 000000000000..3ec382de30c6
--- /dev/null
+++ b/hack/api-docs/testdata/valid.yaml
@@ -0,0 +1,110 @@
+openapi: 3.2.0
+jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
+info:
+ title: Docker documentation tooling fixture
+ version: "1"
+ description: Component capability fixture.
+servers:
+ - url: https://api.example.test/v1
+security:
+ - bearerAuth: []
+tags:
+ - name: operations
+ summary: Operations
+ kind: nav
+ description: Fixture operations.
+paths:
+ /events:
+ get:
+ operationId: listEvents
+ tags:
+ - operations
+ summary: List events
+ description: List events.
+ responses:
+ "200":
+ description: Event stream
+ content:
+ text/event-stream:
+ itemSchema:
+ $ref: ./schemas/event.yaml
+ example: { event: message, data: hello }
+ /search:
+ parameters:
+ - name: limit
+ in: query
+ description: Maximum number of results.
+ schema:
+ type: integer
+ example: 10
+ servers:
+ - url: https://search.example.test
+ query:
+ operationId: querySearch
+ tags:
+ - operations
+ summary: Search
+ description: Search.
+ parameters:
+ - name: limit
+ in: query
+ description: Maximum number of results.
+ schema:
+ type: integer
+ example: 0
+ responses:
+ "200":
+ description: Search result
+ content:
+ application/json:
+ schema: true
+ example: 0
+ /public:
+ get:
+ operationId: getPublic
+ tags:
+ - operations
+ summary: Public result
+ description: Public result.
+ security: []
+ responses:
+ "200":
+ description: A flag
+ content:
+ application/json:
+ schema:
+ type: boolean
+ example: false
+ /items:
+ get:
+ operationId: getItems
+ tags:
+ - operations
+ summary: Get items
+ description: Get items.
+ responses:
+ "200":
+ description: Item
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Item"
+ description: Description beside a reference.
+ example: { id: 1, next: { id: 2, next: null } }
+components:
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ schemas:
+ Item:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ next:
+ anyOf:
+ - $ref: "#/components/schemas/Item"
+ - type: "null"
diff --git a/hack/api-docs/validation/dialects/lock.json b/hack/api-docs/validation/dialects/lock.json
new file mode 100644
index 000000000000..7fb2c1b1cdad
--- /dev/null
+++ b/hack/api-docs/validation/dialects/lock.json
@@ -0,0 +1,20 @@
+[
+ {
+ "file": "oas-dialect.json",
+ "uri": "https://spec.openapis.org/oas/3.1/dialect/base",
+ "resolvedURL": "https://spec.openapis.org/oas/3.1/dialect/base",
+ "sha256": "8a0e89e365dadbebce2921ce6244340c1090e9d544c60d977e9ad6b97a61227b"
+ },
+ {
+ "file": "oas-vocabulary.json",
+ "uri": "https://spec.openapis.org/oas/3.1/meta/base",
+ "resolvedURL": "https://spec.openapis.org/oas/3.1/meta/base",
+ "sha256": "267a88226e64e96dfc8c89dbd7e863160c84715e0fb893ca1d9fbf9f830f1f54"
+ },
+ {
+ "file": "oas-document.json",
+ "uri": "https://spec.openapis.org/oas/3.2/schema/2025-09-17",
+ "resolvedURL": "https://spec.openapis.org/oas/3.2/schema/2025-09-17",
+ "sha256": "0c9d74bf25f9b9388b2d81e421ef60fdefa9feffa94898dadfc501b342b3bfcc"
+ }
+]
diff --git a/hack/api-docs/validation/dialects/oas-dialect.json b/hack/api-docs/validation/dialects/oas-dialect.json
new file mode 100644
index 000000000000..eae8386e8ad3
--- /dev/null
+++ b/hack/api-docs/validation/dialects/oas-dialect.json
@@ -0,0 +1,25 @@
+{
+ "$id": "https://spec.openapis.org/oas/3.1/dialect/base",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+
+ "title": "OpenAPI 3.1 Schema Object Dialect",
+ "description": "A JSON Schema dialect describing schemas found in OpenAPI documents",
+
+ "$vocabulary": {
+ "https://json-schema.org/draft/2020-12/vocab/core": true,
+ "https://json-schema.org/draft/2020-12/vocab/applicator": true,
+ "https://json-schema.org/draft/2020-12/vocab/unevaluated": true,
+ "https://json-schema.org/draft/2020-12/vocab/validation": true,
+ "https://json-schema.org/draft/2020-12/vocab/meta-data": true,
+ "https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
+ "https://json-schema.org/draft/2020-12/vocab/content": true,
+ "https://spec.openapis.org/oas/3.1/vocab/base": false
+ },
+
+ "$dynamicAnchor": "meta",
+
+ "allOf": [
+ { "$ref": "https://json-schema.org/draft/2020-12/schema" },
+ { "$ref": "https://spec.openapis.org/oas/3.1/meta/base" }
+ ]
+}
diff --git a/hack/api-docs/validation/dialects/oas-document.json b/hack/api-docs/validation/dialects/oas-document.json
new file mode 100644
index 000000000000..9453eeb20bec
--- /dev/null
+++ b/hack/api-docs/validation/dialects/oas-document.json
@@ -0,0 +1,1666 @@
+{
+ "$id": "https://spec.openapis.org/oas/3.2/schema/2025-09-17",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "The description of OpenAPI v3.2.x Documents without Schema Object validation",
+ "type": "object",
+ "properties": {
+ "openapi": {
+ "type": "string",
+ "pattern": "^3\\.2\\.\\d+(-.+)?$"
+ },
+ "$self": {
+ "type": "string",
+ "format": "uri-reference",
+ "$comment": "MUST NOT contain a fragment",
+ "pattern": "^[^#]*$"
+ },
+ "info": {
+ "$ref": "#/$defs/info"
+ },
+ "jsonSchemaDialect": {
+ "type": "string",
+ "format": "uri-reference",
+ "default": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17"
+ },
+ "servers": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/server"
+ },
+ "default": [
+ {
+ "url": "/"
+ }
+ ]
+ },
+ "paths": {
+ "$ref": "#/$defs/paths"
+ },
+ "webhooks": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/path-item"
+ }
+ },
+ "components": {
+ "$ref": "#/$defs/components"
+ },
+ "security": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/security-requirement"
+ }
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/tag"
+ }
+ },
+ "externalDocs": {
+ "$ref": "#/$defs/external-documentation"
+ }
+ },
+ "required": [
+ "openapi",
+ "info"
+ ],
+ "anyOf": [
+ {
+ "required": [
+ "paths"
+ ]
+ },
+ {
+ "required": [
+ "components"
+ ]
+ },
+ {
+ "required": [
+ "webhooks"
+ ]
+ }
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false,
+ "$defs": {
+ "info": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#info-object",
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "summary": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "termsOfService": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "contact": {
+ "$ref": "#/$defs/contact"
+ },
+ "license": {
+ "$ref": "#/$defs/license"
+ },
+ "version": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "title",
+ "version"
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "contact": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#contact-object",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "email": {
+ "type": "string",
+ "format": "email"
+ }
+ },
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "license": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#license-object",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "identifier": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri-reference"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "dependentSchemas": {
+ "identifier": {
+ "not": {
+ "required": [
+ "url"
+ ]
+ }
+ }
+ },
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "server": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#server-object",
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "variables": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/server-variable"
+ }
+ }
+ },
+ "required": [
+ "url"
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "server-variable": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#server-variable-object",
+ "type": "object",
+ "properties": {
+ "enum": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1
+ },
+ "default": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "default"
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "components": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#components-object",
+ "type": "object",
+ "properties": {
+ "schemas": {
+ "type": "object",
+ "additionalProperties": {
+ "$dynamicRef": "#meta"
+ }
+ },
+ "responses": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/response-or-reference"
+ }
+ },
+ "parameters": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/parameter-or-reference"
+ }
+ },
+ "examples": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/example-or-reference"
+ }
+ },
+ "requestBodies": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/request-body-or-reference"
+ }
+ },
+ "headers": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/header-or-reference"
+ }
+ },
+ "securitySchemes": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/security-scheme-or-reference"
+ }
+ },
+ "links": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/link-or-reference"
+ }
+ },
+ "callbacks": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/callbacks-or-reference"
+ }
+ },
+ "pathItems": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/path-item"
+ }
+ },
+ "mediaTypes": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/media-type-or-reference"
+ }
+ }
+ },
+ "patternProperties": {
+ "^(?:schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems|mediaTypes)$": {
+ "$comment": "Enumerating all of the property names in the regex above is necessary for unevaluatedProperties to work as expected",
+ "propertyNames": {
+ "pattern": "^[a-zA-Z0-9._-]+$"
+ }
+ }
+ },
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "paths": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#paths-object",
+ "type": "object",
+ "patternProperties": {
+ "^/": {
+ "$ref": "#/$defs/path-item"
+ }
+ },
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "path-item": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#path-item-object",
+ "type": "object",
+ "properties": {
+ "$ref": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "summary": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "servers": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/server"
+ }
+ },
+ "parameters": {
+ "$ref": "#/$defs/parameters"
+ },
+ "additionalOperations": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/operation"
+ },
+ "propertyNames": {
+ "$comment": "RFC9110 restricts methods to \"1*tchar\" in ABNF",
+ "pattern": "^[a-zA-Z0-9!#$%&'*+.^_`|~-]+$",
+ "not": {
+ "enum": [
+ "GET",
+ "PUT",
+ "POST",
+ "DELETE",
+ "OPTIONS",
+ "HEAD",
+ "PATCH",
+ "TRACE",
+ "QUERY"
+ ]
+ }
+ }
+ },
+ "get": {
+ "$ref": "#/$defs/operation"
+ },
+ "put": {
+ "$ref": "#/$defs/operation"
+ },
+ "post": {
+ "$ref": "#/$defs/operation"
+ },
+ "delete": {
+ "$ref": "#/$defs/operation"
+ },
+ "options": {
+ "$ref": "#/$defs/operation"
+ },
+ "head": {
+ "$ref": "#/$defs/operation"
+ },
+ "patch": {
+ "$ref": "#/$defs/operation"
+ },
+ "trace": {
+ "$ref": "#/$defs/operation"
+ },
+ "query": {
+ "$ref": "#/$defs/operation"
+ }
+ },
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "operation": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#operation-object",
+ "type": "object",
+ "properties": {
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "summary": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "externalDocs": {
+ "$ref": "#/$defs/external-documentation"
+ },
+ "operationId": {
+ "type": "string"
+ },
+ "parameters": {
+ "$ref": "#/$defs/parameters"
+ },
+ "requestBody": {
+ "$ref": "#/$defs/request-body-or-reference"
+ },
+ "responses": {
+ "$ref": "#/$defs/responses"
+ },
+ "callbacks": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/callbacks-or-reference"
+ }
+ },
+ "deprecated": {
+ "default": false,
+ "type": "boolean"
+ },
+ "security": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/security-requirement"
+ }
+ },
+ "servers": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/server"
+ }
+ }
+ },
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "external-documentation": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#external-documentation-object",
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri-reference"
+ }
+ },
+ "required": [
+ "url"
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "parameters": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/parameter-or-reference"
+ },
+ "not": {
+ "allOf": [
+ {
+ "contains": {
+ "type": "object",
+ "properties": {
+ "in": {
+ "const": "query"
+ }
+ },
+ "required": [
+ "in"
+ ]
+ }
+ },
+ {
+ "contains": {
+ "type": "object",
+ "properties": {
+ "in": {
+ "const": "querystring"
+ }
+ },
+ "required": [
+ "in"
+ ]
+ }
+ }
+ ]
+ },
+ "contains": {
+ "type": "object",
+ "properties": {
+ "in": {
+ "const": "querystring"
+ }
+ },
+ "required": [
+ "in"
+ ]
+ },
+ "minContains": 0,
+ "maxContains": 1
+ },
+ "parameter": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#parameter-object",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "in": {
+ "enum": [
+ "query",
+ "querystring",
+ "header",
+ "path",
+ "cookie"
+ ]
+ },
+ "description": {
+ "type": "string"
+ },
+ "required": {
+ "default": false,
+ "type": "boolean"
+ },
+ "deprecated": {
+ "default": false,
+ "type": "boolean"
+ },
+ "schema": {
+ "$dynamicRef": "#meta"
+ },
+ "content": {
+ "$ref": "#/$defs/content",
+ "minProperties": 1,
+ "maxProperties": 1
+ }
+ },
+ "required": [
+ "name",
+ "in"
+ ],
+ "oneOf": [
+ {
+ "required": [
+ "schema"
+ ]
+ },
+ {
+ "required": [
+ "content"
+ ]
+ }
+ ],
+ "allOf": [
+ {
+ "$ref": "#/$defs/examples"
+ },
+ {
+ "$ref": "#/$defs/specification-extensions"
+ },
+ {
+ "if": {
+ "properties": {
+ "in": {
+ "const": "query"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "allowEmptyValue": {
+ "default": false,
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "in": {
+ "const": "querystring"
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "content"
+ ]
+ }
+ }
+ ],
+ "dependentSchemas": {
+ "schema": {
+ "properties": {
+ "style": {
+ "type": "string"
+ },
+ "explode": {
+ "type": "boolean"
+ },
+ "allowReserved": {
+ "default": false,
+ "type": "boolean"
+ }
+ },
+ "allOf": [
+ {
+ "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-path"
+ },
+ {
+ "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-header"
+ },
+ {
+ "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-query"
+ },
+ {
+ "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie"
+ },
+ {
+ "$ref": "#/$defs/styles-for-form"
+ }
+ ],
+ "$defs": {
+ "styles-for-path": {
+ "if": {
+ "properties": {
+ "in": {
+ "const": "path"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "style": {
+ "default": "simple",
+ "enum": [
+ "matrix",
+ "label",
+ "simple"
+ ]
+ },
+ "required": {
+ "const": true
+ }
+ },
+ "required": [
+ "required"
+ ]
+ }
+ },
+ "styles-for-header": {
+ "if": {
+ "properties": {
+ "in": {
+ "const": "header"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "style": {
+ "default": "simple",
+ "const": "simple"
+ }
+ }
+ }
+ },
+ "styles-for-query": {
+ "if": {
+ "properties": {
+ "in": {
+ "const": "query"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "style": {
+ "default": "form",
+ "enum": [
+ "form",
+ "spaceDelimited",
+ "pipeDelimited",
+ "deepObject"
+ ]
+ }
+ }
+ }
+ },
+ "styles-for-cookie": {
+ "if": {
+ "properties": {
+ "in": {
+ "const": "cookie"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "style": {
+ "default": "form",
+ "enum": [
+ "form",
+ "cookie"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "unevaluatedProperties": false
+ },
+ "parameter-or-reference": {
+ "if": {
+ "type": "object",
+ "required": [
+ "$ref"
+ ]
+ },
+ "then": {
+ "$ref": "#/$defs/reference"
+ },
+ "else": {
+ "$ref": "#/$defs/parameter"
+ }
+ },
+ "request-body": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#request-body-object",
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "content": {
+ "$ref": "#/$defs/content"
+ },
+ "required": {
+ "default": false,
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "content"
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "request-body-or-reference": {
+ "if": {
+ "type": "object",
+ "required": [
+ "$ref"
+ ]
+ },
+ "then": {
+ "$ref": "#/$defs/reference"
+ },
+ "else": {
+ "$ref": "#/$defs/request-body"
+ }
+ },
+ "content": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#fixed-fields-10",
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/media-type-or-reference"
+ },
+ "propertyNames": {
+ "format": "media-range"
+ }
+ },
+ "media-type": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#media-type-object",
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "schema": {
+ "$dynamicRef": "#meta"
+ },
+ "itemSchema": {
+ "$dynamicRef": "#meta"
+ },
+ "encoding": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/encoding"
+ }
+ },
+ "prefixEncoding": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/encoding"
+ }
+ },
+ "itemEncoding": {
+ "$ref": "#/$defs/encoding"
+ }
+ },
+ "dependentSchemas": {
+ "encoding": {
+ "properties": {
+ "prefixEncoding": false,
+ "itemEncoding": false
+ }
+ }
+ },
+ "allOf": [
+ {
+ "$ref": "#/$defs/examples"
+ },
+ {
+ "$ref": "#/$defs/specification-extensions"
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "media-type-or-reference": {
+ "if": {
+ "type": "object",
+ "required": [
+ "$ref"
+ ]
+ },
+ "then": {
+ "$ref": "#/$defs/reference"
+ },
+ "else": {
+ "$ref": "#/$defs/media-type"
+ }
+ },
+ "encoding": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#encoding-object",
+ "type": "object",
+ "properties": {
+ "contentType": {
+ "type": "string",
+ "format": "media-range"
+ },
+ "headers": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/header-or-reference"
+ }
+ },
+ "style": {
+ "enum": [
+ "form",
+ "spaceDelimited",
+ "pipeDelimited",
+ "deepObject"
+ ]
+ },
+ "explode": {
+ "type": "boolean"
+ },
+ "allowReserved": {
+ "type": "boolean"
+ },
+ "encoding": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/encoding"
+ }
+ },
+ "prefixEncoding": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/encoding"
+ }
+ },
+ "itemEncoding": {
+ "$ref": "#/$defs/encoding"
+ }
+ },
+ "dependentSchemas": {
+ "encoding": {
+ "properties": {
+ "prefixEncoding": false,
+ "itemEncoding": false
+ }
+ },
+ "style": {
+ "properties": {
+ "allowReserved": {
+ "default": false
+ }
+ }
+ },
+ "explode": {
+ "properties": {
+ "style": {
+ "default": "form"
+ },
+ "allowReserved": {
+ "default": false
+ }
+ }
+ },
+ "allowReserved": {
+ "properties": {
+ "style": {
+ "default": "form"
+ }
+ }
+ }
+ },
+ "allOf": [
+ {
+ "$ref": "#/$defs/specification-extensions"
+ },
+ {
+ "$ref": "#/$defs/styles-for-form"
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "responses": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#responses-object",
+ "type": "object",
+ "properties": {
+ "default": {
+ "$ref": "#/$defs/response-or-reference"
+ }
+ },
+ "patternProperties": {
+ "^[1-5](?:[0-9]{2}|XX)$": {
+ "$ref": "#/$defs/response-or-reference"
+ }
+ },
+ "minProperties": 1,
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false,
+ "if": {
+ "$comment": "either default, or at least one response code property must exist",
+ "patternProperties": {
+ "^[1-5](?:[0-9]{2}|XX)$": false
+ }
+ },
+ "then": {
+ "required": [
+ "default"
+ ]
+ }
+ },
+ "response": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#response-object",
+ "type": "object",
+ "properties": {
+ "summary": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "headers": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/header-or-reference"
+ }
+ },
+ "content": {
+ "$ref": "#/$defs/content"
+ },
+ "links": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/link-or-reference"
+ }
+ }
+ },
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "response-or-reference": {
+ "if": {
+ "type": "object",
+ "required": [
+ "$ref"
+ ]
+ },
+ "then": {
+ "$ref": "#/$defs/reference"
+ },
+ "else": {
+ "$ref": "#/$defs/response"
+ }
+ },
+ "callbacks": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#callback-object",
+ "type": "object",
+ "$ref": "#/$defs/specification-extensions",
+ "additionalProperties": {
+ "$ref": "#/$defs/path-item"
+ }
+ },
+ "callbacks-or-reference": {
+ "if": {
+ "type": "object",
+ "required": [
+ "$ref"
+ ]
+ },
+ "then": {
+ "$ref": "#/$defs/reference"
+ },
+ "else": {
+ "$ref": "#/$defs/callbacks"
+ }
+ },
+ "example": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#example-object",
+ "type": "object",
+ "properties": {
+ "summary": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "dataValue": true,
+ "serializedValue": {
+ "type": "string"
+ },
+ "value": true,
+ "externalValue": {
+ "type": "string",
+ "format": "uri-reference"
+ }
+ },
+ "allOf": [
+ {
+ "not": {
+ "required": [
+ "value",
+ "externalValue"
+ ]
+ }
+ },
+ {
+ "not": {
+ "required": [
+ "value",
+ "dataValue"
+ ]
+ }
+ },
+ {
+ "not": {
+ "required": [
+ "value",
+ "serializedValue"
+ ]
+ }
+ },
+ {
+ "not": {
+ "required": [
+ "serializedValue",
+ "externalValue"
+ ]
+ }
+ }
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "example-or-reference": {
+ "if": {
+ "type": "object",
+ "required": [
+ "$ref"
+ ]
+ },
+ "then": {
+ "$ref": "#/$defs/reference"
+ },
+ "else": {
+ "$ref": "#/$defs/example"
+ }
+ },
+ "link": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#link-object",
+ "type": "object",
+ "properties": {
+ "operationRef": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "operationId": {
+ "type": "string"
+ },
+ "parameters": {
+ "$ref": "#/$defs/map-of-strings"
+ },
+ "requestBody": true,
+ "description": {
+ "type": "string"
+ },
+ "server": {
+ "$ref": "#/$defs/server"
+ }
+ },
+ "oneOf": [
+ {
+ "required": [
+ "operationRef"
+ ]
+ },
+ {
+ "required": [
+ "operationId"
+ ]
+ }
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "link-or-reference": {
+ "if": {
+ "type": "object",
+ "required": [
+ "$ref"
+ ]
+ },
+ "then": {
+ "$ref": "#/$defs/reference"
+ },
+ "else": {
+ "$ref": "#/$defs/link"
+ }
+ },
+ "header": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#header-object",
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "required": {
+ "default": false,
+ "type": "boolean"
+ },
+ "deprecated": {
+ "default": false,
+ "type": "boolean"
+ },
+ "schema": {
+ "$dynamicRef": "#meta"
+ },
+ "content": {
+ "$ref": "#/$defs/content",
+ "minProperties": 1,
+ "maxProperties": 1
+ }
+ },
+ "oneOf": [
+ {
+ "required": [
+ "schema"
+ ]
+ },
+ {
+ "required": [
+ "content"
+ ]
+ }
+ ],
+ "dependentSchemas": {
+ "schema": {
+ "properties": {
+ "style": {
+ "default": "simple",
+ "const": "simple"
+ },
+ "explode": {
+ "default": false,
+ "type": "boolean"
+ },
+ "allowReserved": {
+ "default": false,
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "allOf": [
+ {
+ "$ref": "#/$defs/examples"
+ },
+ {
+ "$ref": "#/$defs/specification-extensions"
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "header-or-reference": {
+ "if": {
+ "type": "object",
+ "required": [
+ "$ref"
+ ]
+ },
+ "then": {
+ "$ref": "#/$defs/reference"
+ },
+ "else": {
+ "$ref": "#/$defs/header"
+ }
+ },
+ "tag": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#tag-object",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "summary": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "externalDocs": {
+ "$ref": "#/$defs/external-documentation"
+ },
+ "parent": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "reference": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#reference-object",
+ "type": "object",
+ "properties": {
+ "$ref": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "summary": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ }
+ },
+ "schema": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#schema-object",
+ "$dynamicAnchor": "meta",
+ "type": [
+ "object",
+ "boolean"
+ ]
+ },
+ "security-scheme": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#security-scheme-object",
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "apiKey",
+ "http",
+ "mutualTLS",
+ "oauth2",
+ "openIdConnect"
+ ]
+ },
+ "description": {
+ "type": "string"
+ },
+ "deprecated": {
+ "default": false,
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "type"
+ ],
+ "allOf": [
+ {
+ "$ref": "#/$defs/specification-extensions"
+ },
+ {
+ "$ref": "#/$defs/security-scheme/$defs/type-apikey"
+ },
+ {
+ "$ref": "#/$defs/security-scheme/$defs/type-http"
+ },
+ {
+ "$ref": "#/$defs/security-scheme/$defs/type-http-bearer"
+ },
+ {
+ "$ref": "#/$defs/security-scheme/$defs/type-oauth2"
+ },
+ {
+ "$ref": "#/$defs/security-scheme/$defs/type-oidc"
+ }
+ ],
+ "unevaluatedProperties": false,
+ "$defs": {
+ "type-apikey": {
+ "if": {
+ "properties": {
+ "type": {
+ "const": "apiKey"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "in": {
+ "enum": [
+ "query",
+ "header",
+ "cookie"
+ ]
+ }
+ },
+ "required": [
+ "name",
+ "in"
+ ]
+ }
+ },
+ "type-http": {
+ "if": {
+ "properties": {
+ "type": {
+ "const": "http"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "scheme": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "scheme"
+ ]
+ }
+ },
+ "type-http-bearer": {
+ "if": {
+ "properties": {
+ "type": {
+ "const": "http"
+ },
+ "scheme": {
+ "type": "string",
+ "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$"
+ }
+ },
+ "required": [
+ "type",
+ "scheme"
+ ]
+ },
+ "then": {
+ "properties": {
+ "bearerFormat": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "type-oauth2": {
+ "if": {
+ "properties": {
+ "type": {
+ "const": "oauth2"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "flows": {
+ "$ref": "#/$defs/oauth-flows"
+ },
+ "oauth2MetadataUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ }
+ },
+ "required": [
+ "flows"
+ ]
+ }
+ },
+ "type-oidc": {
+ "if": {
+ "properties": {
+ "type": {
+ "const": "openIdConnect"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "openIdConnectUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ }
+ },
+ "required": [
+ "openIdConnectUrl"
+ ]
+ }
+ }
+ }
+ },
+ "security-scheme-or-reference": {
+ "if": {
+ "type": "object",
+ "required": [
+ "$ref"
+ ]
+ },
+ "then": {
+ "$ref": "#/$defs/reference"
+ },
+ "else": {
+ "$ref": "#/$defs/security-scheme"
+ }
+ },
+ "oauth-flows": {
+ "type": "object",
+ "properties": {
+ "implicit": {
+ "$ref": "#/$defs/oauth-flows/$defs/implicit"
+ },
+ "password": {
+ "$ref": "#/$defs/oauth-flows/$defs/password"
+ },
+ "clientCredentials": {
+ "$ref": "#/$defs/oauth-flows/$defs/client-credentials"
+ },
+ "authorizationCode": {
+ "$ref": "#/$defs/oauth-flows/$defs/authorization-code"
+ },
+ "deviceAuthorization": {
+ "$ref": "#/$defs/oauth-flows/$defs/device-authorization"
+ }
+ },
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false,
+ "$defs": {
+ "implicit": {
+ "type": "object",
+ "properties": {
+ "authorizationUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "refreshUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "scopes": {
+ "$ref": "#/$defs/map-of-strings"
+ }
+ },
+ "required": [
+ "authorizationUrl",
+ "scopes"
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "password": {
+ "type": "object",
+ "properties": {
+ "tokenUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "refreshUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "scopes": {
+ "$ref": "#/$defs/map-of-strings"
+ }
+ },
+ "required": [
+ "tokenUrl",
+ "scopes"
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "client-credentials": {
+ "type": "object",
+ "properties": {
+ "tokenUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "refreshUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "scopes": {
+ "$ref": "#/$defs/map-of-strings"
+ }
+ },
+ "required": [
+ "tokenUrl",
+ "scopes"
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "authorization-code": {
+ "type": "object",
+ "properties": {
+ "authorizationUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "tokenUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "refreshUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "scopes": {
+ "$ref": "#/$defs/map-of-strings"
+ }
+ },
+ "required": [
+ "authorizationUrl",
+ "tokenUrl",
+ "scopes"
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ },
+ "device-authorization": {
+ "type": "object",
+ "properties": {
+ "deviceAuthorizationUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "tokenUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "refreshUrl": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "scopes": {
+ "$ref": "#/$defs/map-of-strings"
+ }
+ },
+ "required": [
+ "deviceAuthorizationUrl",
+ "tokenUrl",
+ "scopes"
+ ],
+ "$ref": "#/$defs/specification-extensions",
+ "unevaluatedProperties": false
+ }
+ }
+ },
+ "security-requirement": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#security-requirement-object",
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "specification-extensions": {
+ "$comment": "https://spec.openapis.org/oas/v3.2#specification-extensions",
+ "patternProperties": {
+ "^x-": true
+ }
+ },
+ "examples": {
+ "properties": {
+ "example": true,
+ "examples": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/example-or-reference"
+ }
+ }
+ },
+ "not": {
+ "required": [
+ "example",
+ "examples"
+ ]
+ }
+ },
+ "map-of-strings": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "styles-for-form": {
+ "if": {
+ "properties": {
+ "style": {
+ "const": "form"
+ }
+ },
+ "required": [
+ "style"
+ ]
+ },
+ "then": {
+ "properties": {
+ "explode": {
+ "default": true
+ }
+ }
+ },
+ "else": {
+ "properties": {
+ "explode": {
+ "default": false
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/hack/api-docs/validation/dialects/oas-vocabulary.json b/hack/api-docs/validation/dialects/oas-vocabulary.json
new file mode 100644
index 000000000000..a7a59f1c7d13
--- /dev/null
+++ b/hack/api-docs/validation/dialects/oas-vocabulary.json
@@ -0,0 +1,87 @@
+{
+ "$id": "https://spec.openapis.org/oas/3.1/meta/base",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+
+ "title": "OAS Base vocabulary",
+ "description": "A JSON Schema Vocabulary used in the OpenAPI Schema Dialect",
+
+ "$vocabulary": {
+ "https://spec.openapis.org/oas/3.1/vocab/base": true
+ },
+
+ "$dynamicAnchor": "meta",
+
+ "type": ["object", "boolean"],
+ "properties": {
+ "example": true,
+ "discriminator": { "$ref": "#/$defs/discriminator" },
+ "externalDocs": { "$ref": "#/$defs/external-docs" },
+ "xml": { "$ref": "#/$defs/xml" }
+ },
+
+ "$defs": {
+ "extensible": {
+ "patternProperties": {
+ "^x-": true
+ }
+ },
+
+ "discriminator": {
+ "$ref": "#/$defs/extensible",
+ "type": "object",
+ "properties": {
+ "propertyName": {
+ "type": "string"
+ },
+ "mapping": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["propertyName"],
+ "unevaluatedProperties": false
+ },
+
+ "external-docs": {
+ "$ref": "#/$defs/extensible",
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "required": ["url"],
+ "unevaluatedProperties": false
+ },
+
+ "xml": {
+ "$ref": "#/$defs/extensible",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "namespace": {
+ "type": "string",
+ "format": "uri"
+ },
+ "prefix": {
+ "type": "string"
+ },
+ "attribute": {
+ "type": "boolean"
+ },
+ "wrapped": {
+ "type": "boolean"
+ }
+ },
+ "unevaluatedProperties": false
+ }
+ }
+}
diff --git a/hack/api-docs/validation/rules.yaml b/hack/api-docs/validation/rules.yaml
new file mode 100644
index 000000000000..e3fdaa5d5a43
--- /dev/null
+++ b/hack/api-docs/validation/rules.yaml
@@ -0,0 +1,24 @@
+# Structural and dialect validation run against locked OAI schemas in the Go command.
+# Vacuum supplies independently named Docker policy rules with source locations.
+rules:
+ docker-operation-id:
+ description: Every operation needs a stable operation ID.
+ severity: error
+ given: $.paths[*][get,put,post,delete,options,head,patch,trace,query]
+ then:
+ field: operationId
+ function: truthy
+ docker-info-description:
+ description: Every API needs an overview.
+ severity: error
+ given: $.info
+ then:
+ field: description
+ function: truthy
+ docker-info-version:
+ description: Every API needs a version identity.
+ severity: error
+ given: $.info
+ then:
+ field: version
+ function: truthy
diff --git a/hack/api-docs/verify-output.mjs b/hack/api-docs/verify-output.mjs
new file mode 100644
index 000000000000..33001b850d7d
--- /dev/null
+++ b/hack/api-docs/verify-output.mjs
@@ -0,0 +1,158 @@
+import fs from "node:fs";
+import path from "node:path";
+import { isDeepStrictEqual } from "node:util";
+const root = path.resolve(import.meta.dirname, "../..");
+const base = path.resolve(process.argv[2] || "public");
+const data = JSON.parse(
+ fs.readFileSync(path.join(root, "tmp/api-reference/data/api-reference.json")),
+);
+const decode = (s) =>
+ s
+ .replace(/(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
+ .replaceAll("&", "&")
+ .replaceAll(""", '"')
+ .replaceAll("'", "'")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">");
+// Hugo's production minifier removes optional attribute quotes.
+function attributes(html, name) {
+ const pattern = new RegExp(
+ `\\s${name}=(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`,
+ "g",
+ );
+ return [...html.matchAll(pattern)].map((m) => decode(m[1] ?? m[2] ?? m[3]));
+}
+let count = 0;
+const problems = [];
+function check(url) {
+ const htmlPath = path.join(base, url, "index.html");
+ const mdPath = path.join(base, url.replace(/\/$/, "")) + ".md";
+ if (!fs.existsSync(htmlPath) || !fs.existsSync(mdPath)) {
+ problems.push(`Missing HTML/Markdown: ${url}`);
+ return ["", ""];
+ }
+ const html = fs.readFileSync(htmlPath, "utf8"),
+ md = fs.readFileSync(mdPath, "utf8");
+ count++;
+ return [decode(html), md];
+}
+check("/reference/api/");
+for (const api of data.apis) {
+ check(api.url);
+ const publishedSource = fs.readFileSync(path.join(base, api.sourceURL));
+ if (!publishedSource.equals(fs.readFileSync(path.join(root, api.source))))
+ problems.push(`Published specification differs from source: ${api.id}`);
+
+ for (const op of api.operations) {
+ const [html, md] = check(op.url);
+ for (const variant of op.variants) {
+ if (
+ !attributes(html, "data-api-variant").includes(variant.pointer) ||
+ !md.includes(`### ${variant.direction} ${variant.status}`)
+ )
+ problems.push(`Missing variant: ${op.url} ${variant.pointer}`);
+ for (const example of variant.examples) {
+ if (
+ !html.includes(example.text.trim()) ||
+ !md.includes("```" + example.language + "\n" + example.text)
+ )
+ problems.push(`Example mismatch: ${op.url} ${variant.pointer}`);
+ }
+ }
+ for (const p of op.parameters)
+ if (
+ !attributes(html, "data-api-parameter").includes(p.name) ||
+ !md.includes(`### ${p.name}`)
+ )
+ problems.push(`Missing parameter: ${op.url} ${p.name}`);
+ if (!html.includes(op.path) || !md.includes(op.method + " " + op.path))
+ problems.push(`Missing operation signature: ${op.url}`);
+ if (!md.includes(op.curl))
+ problems.push(`Request example mismatch: ${op.url}`);
+ }
+ for (const schema of api.schemas) {
+ const [html, md] = check(schema.url);
+ if (
+ ![...md.matchAll(/```json\n([\s\S]*?)\n```/g)].some((m) => {
+ try {
+ return isDeepStrictEqual(JSON.parse(m[1]), schema.schema);
+ } catch {
+ return false;
+ }
+ })
+ )
+ problems.push(`Incomplete Markdown schema: ${schema.url}`);
+ if (!html.includes(schema.name))
+ problems.push(`Missing schema heading: ${schema.url}`);
+ }
+}
+// Check API reference links in every generated preview page, including copied specification prose.
+function files(dir) {
+ return fs
+ .readdirSync(dir, { withFileTypes: true })
+ .flatMap((e) =>
+ e.isDirectory()
+ ? files(path.join(dir, e.name))
+ : [path.join(dir, e.name)],
+ );
+}
+for (const file of [...files(path.join(base, "reference/api"))].filter((p) =>
+ p.endsWith(".html"),
+)) {
+ const html = fs.readFileSync(file, "utf8");
+ if (!html.includes("data-api-view=")) continue;
+ for (const href of attributes(html, "href")) {
+ let url;
+ try {
+ url = new URL(href, "http://localhost:1314/" + path.relative(base, file));
+ } catch {
+ continue;
+ }
+ if (
+ url.hostname !== "localhost" ||
+ !url.pathname.startsWith("/reference/api/")
+ )
+ continue;
+ const target = path.join(
+ base,
+ decodeURIComponent(url.pathname),
+ url.pathname.endsWith("/") ? "index.html" : "",
+ );
+ if (!fs.existsSync(target))
+ problems.push(`Broken API link: ${path.relative(base, file)} -> ${href}`);
+ }
+}
+const governance = fs.readFileSync(
+ path.join(base, "reference/api/ai-governance/index.html"),
+ "utf8",
+);
+if (governance.includes("data-api-view="))
+ problems.push("Governance renderer changed");
+if (fs.existsSync(path.join(base, "api-prototype")))
+ problems.push("Unexpected prototype routes");
+// Historical Engine pages must keep the original renderer at their existing URLs.
+for (let minor = 40; minor <= 56; minor++) {
+ const url = `/reference/api/engine/version/v1.${minor}/`;
+ const html = fs.readFileSync(path.join(base, url, "index.html"), "utf8");
+ if (!html.includes(" n + fs.statSync(p).size,
+ 0,
+);
+fs.writeFileSync(
+ path.join(root, "tmp/api-reference/reports/output.json"),
+ JSON.stringify(
+ { htmlMarkdownPagePairs: count, referenceBytes: bytes, result: "pass" },
+ null,
+ 2,
+ ) + "\n",
+);
+console.log(
+ `Verified ${count} HTML/Markdown page pairs and API reference links.`,
+);
diff --git a/hack/flatten-and-resolve.js b/hack/flatten-and-resolve.js
index 04532d343eb5..b79ca5e21208 100755
--- a/hack/flatten-and-resolve.js
+++ b/hack/flatten-and-resolve.js
@@ -1,236 +1,99 @@
#!/usr/bin/env node
/**
- * Flattens markdown directory structure and resolves all links to absolute paths.
- *
- * This script:
- * 1. Moves index.md files up one level (ai/model-runner/index.md -> ai/model-runner.md)
- * 2. Fixes _index.md and index.md references in links
- * 3. Strips /manuals/ prefix from paths (Hugo config removes this)
- * 4. Resolves all relative links to absolute HTML paths for RAG ingestion
- *
- * Usage: node flatten-and-resolve.js [public-dir]
+ * Resolve Markdown links relative to each original file, then flatten index.md.
+ * Only link destinations change; examples and other text stay byte-identical.
+ * Usage: node hack/flatten-and-resolve.js [public-dir]
*/
-
-const fs = require('fs');
-const path = require('path');
-
-const PUBLIC_DIR = path.resolve(process.argv[2] || 'public');
-
-if (!fs.existsSync(PUBLIC_DIR)) {
- console.error(`Error: Directory ${PUBLIC_DIR} does not exist`);
- process.exit(1);
+const fs = require("node:fs");
+const path = require("node:path");
+
+const publicDir = path.resolve(process.argv[2] || "public");
+
+function markdownFiles(dir) {
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
+ const file = path.join(dir, entry.name);
+ return entry.isDirectory()
+ ? markdownFiles(file)
+ : file.endsWith(".md")
+ ? [file]
+ : [];
+ });
}
-/**
- * Recursively find all files matching a predicate
- */
-function findFiles(dir, predicate) {
- const results = [];
- const entries = fs.readdirSync(dir, { withFileTypes: true });
-
- for (const entry of entries) {
- const fullPath = path.join(dir, entry.name);
- if (entry.isDirectory()) {
- results.push(...findFiles(fullPath, predicate));
- } else if (entry.isFile() && predicate(entry.name)) {
- results.push(fullPath);
- }
- }
-
- return results;
+function resolveLink(link, file) {
+ // External URLs, protocol-relative URLs, and same-page anchors are complete.
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(link)) return link;
+ const [, pathname, suffix] = link.match(/^([^?#]*)(.*)$/s);
+ if (!pathname) return link;
+ let url = pathname.startsWith("/")
+ ? pathname
+ : "/" +
+ path
+ .relative(publicDir, path.resolve(path.dirname(file), pathname))
+ .split(path.sep)
+ .join("/");
+ url = url.replace(/^\/manuals\//, "/");
+ url = url.replace(/\/_?index\.md$/, "/").replace(/\.md$/, "/");
+ return url + suffix;
}
-/**
- * Step 1: Flatten index.md files
- * Move path/to/section/index.md -> path/to/section.md
- * Before moving, rewrite sibling links (e.g., "get-started.md" -> "section/get-started.md")
- */
-function flattenIndexFiles() {
- const indexFiles = findFiles(PUBLIC_DIR, name => name === 'index.md');
- let count = 0;
-
- for (const file of indexFiles) {
- // Skip root index.md
- if (file === path.join(PUBLIC_DIR, 'index.md')) {
- continue;
- }
-
- const dir = path.dirname(file);
- const dirname = path.basename(dir);
-
- // Read content and fix sibling links
- let content = fs.readFileSync(file, 'utf8');
-
- // Rewrite relative links that don't start with /, ../, or http
- // These are sibling files that will become children after flattening
- content = content.replace(
- /\[([^\]]+)\]\(([a-zA-Z0-9][^):]*)\)/g,
- (match, text, link) => {
- // Skip if it's a URL or starts with special chars
- if (link.startsWith('http://') || link.startsWith('https://') ||
- link.startsWith('#')) {
- return match;
- }
- return `[${text}](${dirname}/${link})`;
- }
- );
-
- // Also fix reference-style links
- content = content.replace(
- /^\[([^\]]+)\]:\s+([a-zA-Z0-9][^: ]*\.md)$/gm,
- (match, ref, link) => `[${ref}]: ${dirname}/${link}`
- );
-
- fs.writeFileSync(file, content, 'utf8');
-
- // Move file up one level
- const parentDir = path.dirname(dir);
- const newPath = path.join(parentDir, `${dirname}.md`);
- fs.renameSync(file, newPath);
-
- count++;
+// Micromark provides exact destination spans, excluding labels, titles, and code.
+// Apply edits backwards so offsets stay valid and formatting stays untouched.
+function rewriteLinks(content, events, file) {
+ const destinations = events
+ .filter(
+ ([event, token]) =>
+ event === "enter" &&
+ ["resourceDestinationString", "definitionDestinationString"].includes(
+ token.type,
+ ),
+ )
+ .map(([, token]) => token)
+ .sort((a, b) => b.start.offset - a.start.offset);
+ for (const token of destinations) {
+ const start = token.start.offset;
+ const end = token.end.offset;
+ const destination = content.slice(start, end);
+ content =
+ content.slice(0, start) +
+ resolveLink(destination, file) +
+ content.slice(end);
}
-
- console.log(`Flattened ${count} index.md files`);
- return count;
+ return content;
}
-/**
- * Step 2: Fix _index.md and index.md references in all files
- * Also strip /manuals/ prefix from paths
- */
-function fixIndexReferences() {
- const mdFiles = findFiles(PUBLIC_DIR, name => name.endsWith('.md'));
- let count = 0;
-
- for (const file of mdFiles) {
- const dir = path.dirname(file);
- const dirname = path.basename(dir);
- const parentDir = path.dirname(dir);
- const parentDirname = path.basename(parentDir);
-
- let content = fs.readFileSync(file, 'utf8');
- const original = content;
-
- // Fix path/_index.md or path/index.md -> path.md
- content = content.replace(/([a-zA-Z0-9_/-]+)\/_?index\.md/g, '$1.md');
-
- // Fix bare _index.md or index.md -> ../dirname.md
- content = content.replace(/_?index\.md/g, `../${dirname}.md`);
-
- // Fix ../_index.md that became ...md -> ../../parentdirname.md
- if (parentDir !== PUBLIC_DIR) {
- content = content.replace(/\.\.\.md/g, `../../${parentDirname}.md`);
- }
-
- // Strip /manuals/ prefix (both /manuals/ and manuals/)
- content = content.replace(/\/?manuals\//g, '/');
-
+async function main() {
+ const { parse, preprocess, postprocess } = await import("micromark");
+ const files = markdownFiles(publicDir);
+ let rewritten = 0;
+ let flattened = 0;
+ for (const file of files) {
+ const original = fs.readFileSync(file, "utf8");
+ const events = postprocess(
+ parse()
+ .document()
+ .write(preprocess()(original, "utf8", true)),
+ );
+ const content = rewriteLinks(original, events, file);
if (content !== original) {
- fs.writeFileSync(file, content, 'utf8');
- count++;
+ fs.writeFileSync(file, content);
+ rewritten++;
}
- }
-
- console.log(`Fixed _index.md references in ${count} files`);
- return count;
-}
-
-/**
- * Step 3: Resolve all relative links to absolute HTML paths
- */
-function resolveLinks() {
- const mdFiles = findFiles(PUBLIC_DIR, name => name.endsWith('.md'));
- let count = 0;
-
- for (const file of mdFiles) {
- let content = fs.readFileSync(file, 'utf8');
- const original = content;
-
- // Process inline links: [text](path)
- content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, link) => {
- const resolved = resolveLinkPath(link, file);
- return `[${text}](${resolved})`;
- });
-
- // Process reference-style links: [ref]: path
- content = content.replace(/^\[([^\]]+)\]:\s+(.+)$/gm, (match, ref, link) => {
- const resolved = resolveLinkPath(link, file);
- return `[${ref}]: ${resolved}`;
- });
-
- if (content !== original) {
- fs.writeFileSync(file, content, 'utf8');
- count++;
+ if (
+ path.basename(file) === "index.md" &&
+ file !== path.join(publicDir, "index.md")
+ ) {
+ fs.renameSync(file, path.dirname(file) + ".md");
+ flattened++;
}
}
-
- console.log(`Resolved links in ${count} files`);
- return count;
-}
-
-/**
- * Resolve a link path to absolute HTML format
- */
-function resolveLinkPath(linkPath, currentFile) {
- // Skip external URLs and mailto
- if (linkPath.startsWith('http://') || linkPath.startsWith('https://') ||
- linkPath.startsWith('mailto:')) {
- return linkPath;
- }
-
- // Skip same-page anchors
- if (linkPath.startsWith('#')) {
- return linkPath;
- }
-
- // Split path and anchor
- const hashIndex = linkPath.indexOf('#');
- const pathPart = hashIndex >= 0 ? linkPath.substring(0, hashIndex) : linkPath;
- const anchorPart = hashIndex >= 0 ? linkPath.substring(hashIndex) : '';
-
- if (!pathPart) {
- // Just an anchor
- return linkPath;
- }
-
- // Handle absolute paths - just convert to HTML format
- if (pathPart.startsWith('/')) {
- return toHtmlPath(pathPart) + anchorPart;
- }
-
- // Resolve relative path to absolute
- const currentDir = path.dirname(currentFile);
- const absolutePath = path.resolve(currentDir, pathPart);
- const relativePath = path.relative(PUBLIC_DIR, absolutePath);
-
- // Convert to URL path (forward slashes)
- const urlPath = '/' + relativePath.split(path.sep).join('/');
-
- return toHtmlPath(urlPath) + anchorPart;
-}
-
-/**
- * Convert a path to HTML format (strip .md, add trailing /)
- */
-function toHtmlPath(urlPath) {
- if (urlPath.endsWith('.md')) {
- return urlPath.slice(0, -3) + '/';
- }
- return urlPath;
+ console.log(
+ `Markdown: rewrote links in ${rewritten} files; flattened ${flattened} index.md files`,
+ );
}
-// Main execution
-console.log('Starting markdown flattening and link resolution...');
-console.log('');
-
-const flattenCount = flattenIndexFiles();
-const fixCount = fixIndexReferences();
-const resolveCount = resolveLinks();
-
-console.log('');
-console.log('Done!');
-console.log(`- Flattened: ${flattenCount} files`);
-console.log(`- Fixed references: ${fixCount} files`);
-console.log(`- Resolved links: ${resolveCount} files`);
+main().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/hack/test/flatten-and-resolve.mjs b/hack/test/flatten-and-resolve.mjs
new file mode 100644
index 000000000000..697287c35350
--- /dev/null
+++ b/hack/test/flatten-and-resolve.mjs
@@ -0,0 +1,119 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { execFileSync } from "node:child_process";
+
+const script = path.resolve(import.meta.dirname, "../flatten-and-resolve.js");
+
+test("flattens all pages while preserving examples and resolving documentation links", () => {
+ const site = fs.mkdtempSync(path.join(os.tmpdir(), "docs-flatten-"));
+ const generated =
+ "# API\n\n" +
+ "[Guide](/reference/api/registry/auth/)\n\n" +
+ '```json\n{"path":"/manuals/example/index.md","text":"[sample](file.md)"}\n```\n';
+ const write = (name, value) => {
+ const file = path.join(site, name);
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.writeFileSync(file, value);
+ };
+ try {
+ write("reference/api/index.md", generated);
+ write("reference/api/registry/latest/index.md", generated);
+ write(
+ "reference/api/registry/auth/index.md",
+ "# Guide\n\n[Example](example.md)\n",
+ );
+ write("reference/api/registry/auth/example.md", "# Example\n");
+ for (let run = 0; run < 2; run++) {
+ execFileSync(process.execPath, [script, site], { stdio: "pipe" });
+ assert.equal(
+ fs.readFileSync(path.join(site, "reference/api.md"), "utf8"),
+ generated,
+ );
+ assert.equal(
+ fs.readFileSync(
+ path.join(site, "reference/api/registry/latest.md"),
+ "utf8",
+ ),
+ generated,
+ );
+ assert.ok(
+ !fs.existsSync(
+ path.join(site, "reference/api/registry/latest/index.md"),
+ ),
+ );
+ assert.match(
+ fs.readFileSync(
+ path.join(site, "reference/api/registry/auth.md"),
+ "utf8",
+ ),
+ /\[Example\]\(\/reference\/api\/registry\/auth\/example\/\)/,
+ );
+ }
+ } finally {
+ fs.rmSync(site, { recursive: true, force: true });
+ }
+});
+
+test("rewrites only destinations and preserves Markdown syntax", () => {
+ const site = fs.mkdtempSync(path.join(os.tmpdir(), "docs-links-"));
+ const cases = [
+ ["[Guide](/manuals/engine/_index.md#install)", "[Guide](/engine/#install)"],
+ ["[Parent](../index.md)", "[Parent](/guide/)"],
+ ["[Root](/index.md)", "[Root](/)"],
+ [
+ '[Title]( "A title")',
+ '[Title]( "A title")',
+ ],
+ [
+ "[Nested **label**](file(test).md?q=1#part)",
+ "[Nested **label**](/guide/section/file(test)/?q=1#part)",
+ ],
+ ["[`odd ] bracket`](file.md)", "[`odd ] bracket`](/guide/section/file/)"],
+ [
+ "![Image [label]](picture.png)",
+ "![Image [label]](/guide/section/picture.png)",
+ ],
+ [
+ '[ref]: /manuals/engine/index.md "Title"\n\n[Reference][ref]',
+ '[ref]: /engine/ "Title"\n\n[Reference][ref]',
+ ],
+ ["> - [Guide](file.md)", "> - [Guide](/guide/section/file/)"],
+ [
+ "[External](https://example.com/manuals/index.md)",
+ "[External](https://example.com/manuals/index.md)",
+ ],
+ [
+ "[CDN](//example.com/manuals/index.md)",
+ "[CDN](//example.com/manuals/index.md)",
+ ],
+ ["[Mail](mailto:docs@example.com)", "[Mail](mailto:docs@example.com)"],
+ ["[Anchor](#example)", "[Anchor](#example)"],
+ ["`[Example](/manuals/index.md)`", "`[Example](/manuals/index.md)`"],
+ [" [Example](/manuals/index.md)", " [Example](/manuals/index.md)"],
+ [
+ '~~~json\n{"file":"/manuals/index.md"}\n~~~',
+ '~~~json\n{"file":"/manuals/index.md"}\n~~~',
+ ],
+ [
+ "A literal /manuals/example/index.md filename.",
+ "A literal /manuals/example/index.md filename.",
+ ],
+ ];
+ try {
+ fs.mkdirSync(path.join(site, "guide/section"), { recursive: true });
+ fs.writeFileSync(
+ path.join(site, "guide/section/index.md"),
+ cases.map(([before]) => before).join("\n\n"),
+ );
+ execFileSync(process.execPath, [script, site]);
+ assert.equal(
+ fs.readFileSync(path.join(site, "guide/section.md"), "utf8"),
+ cases.map(([, after]) => after).join("\n\n"),
+ );
+ } finally {
+ fs.rmSync(site, { recursive: true, force: true });
+ }
+});
diff --git a/hugo.yaml b/hugo.yaml
index adef38e31097..831f2a6b2b9c 100644
--- a/hugo.yaml
+++ b/hugo.yaml
@@ -327,6 +327,10 @@ module:
extended: false
min: "0.156.0"
mounts:
+ - source: data
+ target: data
+ - source: tmp/api-reference/data
+ target: data
# Mount the assets directory so it doesn't get overwritten
- source: assets
target: assets
diff --git a/layouts/_partials/api-reference/description.html b/layouts/_partials/api-reference/description.html
new file mode 100644
index 000000000000..1616cc83a7f5
--- /dev/null
+++ b/layouts/_partials/api-reference/description.html
@@ -0,0 +1,13 @@
+{{- $text := .text | default "" -}}
+{{- $api := .api -}}
+{{- range $api.operations -}}
+ {{- $text = replace $text (printf "(#operation/%s)" .id) (printf "(%s)" .url) -}}
+ {{- $text = replaceRE (printf "\\(#tag/[^)]+/operation/%s\\)" .id) (printf "(%s)" .url) $text -}}
+{{- end -}}
+{{- range $api.schemas -}}
+ {{- $text = replace $text (printf "(#schema-%s)" .name) (printf "(%s)" .url) -}}
+{{- end -}}
+{{- range $api.tags -}}
+ {{- $text = replace $text (printf "(#tag/%s)" .name) (printf "(%s#tag-%s)" $api.url (.name | urlize)) -}}
+{{- end -}}
+{{- return $text -}}
diff --git a/layouts/_partials/api-reference/manual-link.html b/layouts/_partials/api-reference/manual-link.html
new file mode 100644
index 000000000000..0b92028d75f7
--- /dev/null
+++ b/layouts/_partials/api-reference/manual-link.html
@@ -0,0 +1,11 @@
+{{ $data := index hugo.Data "api-reference" }}
+{{ range $data.apis }}
+ {{ $manual := site.GetPage .manual }}
+ {{ if and $manual (eq $manual.RelPermalink $.RelPermalink) }}
+
+ Explore the {{ .title }} API {{ .version }} →
+
+ {{ end }}
+{{ end }}
diff --git a/layouts/_partials/api-reference/navigation.html b/layouts/_partials/api-reference/navigation.html
new file mode 100644
index 000000000000..3a814260e04e
--- /dev/null
+++ b/layouts/_partials/api-reference/navigation.html
@@ -0,0 +1,36 @@
+{{ $data := index hugo.Data "api-reference" }}
+
+ ← API catalog
+ {{ range $data.apis }}
+ {{ if eq .id $.Params.apiID }}
+ {{ .title }}
+ API {{ .version }}
+
+ {{ range .tags }}
+ {{ if eq .kind "nav" }}
+ {{ $tag := .name }}
+ {{ .summary }}
+ {{ range (index (where $data.apis "id" $.Params.apiID) 0).operations }}
+ {{ if eq (index .tags 0) $tag }}
+
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+
diff --git a/layouts/_partials/api-reference/overview-tags.html b/layouts/_partials/api-reference/overview-tags.html
new file mode 100644
index 000000000000..796138cc1c00
--- /dev/null
+++ b/layouts/_partials/api-reference/overview-tags.html
@@ -0,0 +1,11 @@
+{{- /* Navigation placeholders and schema-only links are covered by the indexes. */ -}}
+{{- $visible := slice -}}
+{{- range .tags -}}
+ {{- $description := strings.TrimSpace (.description | default "") -}}
+ {{- $placeholder := or (eq $description (printf "%s reference." .summary)) (eq $description (printf "%s operations." .name)) -}}
+ {{- $schemaLink := findRE `^\[[^\]]+\]\(#schema-[^)]+\)$` $description -}}
+ {{- if and $description (not $placeholder) (not $schemaLink) -}}
+ {{- $visible = $visible | append . -}}
+ {{- end -}}
+{{- end -}}
+{{- return $visible -}}
diff --git a/layouts/_partials/api-reference/path.html b/layouts/_partials/api-reference/path.html
new file mode 100644
index 000000000000..cb8045e25430
--- /dev/null
+++ b/layouts/_partials/api-reference/path.html
@@ -0,0 +1,4 @@
+{{- /* Break long paths between segments without changing their copied text. */ -}}
+{{- range $i, $segment := split . "/" -}}
+ {{- if $i -}} /{{- end -}}{{- $segment -}}
+{{- end -}}
diff --git a/layouts/_partials/api-reference/schema.html b/layouts/_partials/api-reference/schema.html
new file mode 100644
index 000000000000..d044f93dafab
--- /dev/null
+++ b/layouts/_partials/api-reference/schema.html
@@ -0,0 +1,85 @@
+{{ $s := .schema }}{{ $api := .api }}{{ $depth := .depth }}
+{{ if reflect.IsMap $s }}
+ {{ with index $s "$ref" }}
+ {{ $url := index $api.schemaURLs . }}
+
+ Schema:
+ {{ if $url }}
+ {{ strings.TrimPrefix "#/components/schemas/" . }}
+ {{ else }}
+ {{ . }}
+ {{ end }}
+
+ {{ end }}
+ {{ with $s.type }}
+
+ Type:
+ {{ if reflect.IsSlice . }}
+ {{ delimit . " | " }}
+ {{ else }}
+ {{ . }}
+ {{ end }}
+
+ {{ end }}
+ {{ with $s.description }}
+ {{ . | markdownify }}
+ {{ end }}
+ {{ if lt $depth 3 }}
+ {{ range $name,$v := $s.properties }}
+
+
+ {{ $name }}{{ if in $s.required $name }}
+ · required
+ {{ end }}{{ if $v.readOnly }}
+ · response only
+ {{ end }}{{ if $v.writeOnly }}· request only{{ end }}
+
+ {{ partial "api-reference/schema.html" (dict "schema" $v "api" $api "depth" (add $depth 1)) }}
+
+ {{ end }}
+ {{ range $keyword := slice "allOf" "oneOf" "anyOf" }}
+ {{ with index $s $keyword }}
+
+
+ {{ if eq $keyword "allOf" }}
+ All constraints apply
+ {{ else if eq $keyword "oneOf" }}
+ Exactly one alternative must match
+ {{ else }}
+ One or more alternatives must match
+ {{ end }}
+
+ {{ range . }}
+ {{ partial "api-reference/schema.html" (dict "schema" . "api" $api "depth" (add $depth 1)) }}
+ {{ end }}
+
+ {{ end }}
+ {{ end }}
+ {{ if isset $s "items" }}
+
+ Array item
+ {{ partial "api-reference/schema.html" (dict "schema" $s.items "api" $api "depth" (add $depth 1)) }}
+
+ {{ end }}
+ {{ end }}
+ {{ if or (ge $depth 3) (not (index $s "$ref")) (gt (len $s) 1) }}
+
+ All schema constraints and annotations
+ {{ $s | jsonify (dict "indent" " ") }}
+
+ {{ end }}
+{{ else }}
+
+ {{ if eq $s false }}
+ No value satisfies this schema.
+ {{ else if eq $s true }}
+ Any value satisfies this schema.
+ {{ else }}
+ No schema is declared.
+ {{ end }}
+
+{{ end }}
diff --git a/layouts/_partials/content-default.html b/layouts/_partials/content-default.html
index 9d7c32067066..73580a30d58c 100644
--- a/layouts/_partials/content-default.html
+++ b/layouts/_partials/content-default.html
@@ -12,6 +12,7 @@ {{ .Title | safeHTML }}
{{ partialCached "pagemeta.html" . . }}
+{{ partial "api-reference/manual-link.html" . }}
{{ .Content }}
{{- if and (eq .Type "guides") (not .IsSection) (ne .CurrentSection .FirstSection) -}}
{{- with .PrevInSection -}}
diff --git a/layouts/_partials/sidebar/sections.html b/layouts/_partials/sidebar/sections.html
index ab1650ff1a86..0bd06b5dcd8a 100644
--- a/layouts/_partials/sidebar/sections.html
+++ b/layouts/_partials/sidebar/sections.html
@@ -30,7 +30,7 @@
{{- end }}
{{- $ungrouped := where $pages "Params.sidebar.group" "==" nil }}
{{- range $ungrouped }}
- {{- if .IsSection }}
+ {{- if and .IsSection (not .Params.apiID) }}
{{- template "renderList" (dict "section" . "revealHidden" $revealHidden) }}
{{- else }}
{{- template "renderSingle" . }}
@@ -43,7 +43,7 @@
{{ . }}
{{- range where $pages "Params.sidebar.group" . }}
- {{- if .IsSection }}
+ {{- if and .IsSection (not .Params.apiID) }}
{{- template "renderList" (dict "section" . "revealHidden" $revealHidden) }}
{{- else }}
{{- template "renderSingle" . }}
diff --git a/layouts/api-docs.html b/layouts/api-docs.html
new file mode 100644
index 000000000000..6303b9fe73e6
--- /dev/null
+++ b/layouts/api-docs.html
@@ -0,0 +1,369 @@
+{{ define "head-extra" }}
+ {{ $style := resources.Get "api-reference/reference.css" | fingerprint }}
+
+ {{ $script := resources.Get "api-reference/reference.js" | fingerprint }}
+
+{{ end }}
+{{ define "left" }}
+ {{ partial "sidebar/mainnav.html" . }}
+ {{ if eq .Params.view "catalog" }}
+ {{ partial "sidebar/sections.html" . }}
+ {{ else }}
+ {{ partial "api-reference/navigation.html" . }}
+ {{ end }}
+{{ end }}
+{{ define "main" }}
+
+ {{ $data := index hugo.Data "api-reference" }}
+ {{ $api := dict }}{{ range $data.apis }}
+ {{ if eq .id $.Params.apiID }}{{ $api = . }}{{ end }}
+ {{ end }}
+ {{ if eq .Params.view "catalog" }}
+ Developer reference
+ Docker APIs
+
+ Build with Docker, from your local daemon to hosted services.
+
+
+ Choose an API to find connection guidance, operations, and data models.
+
+
+ {{ else }}
+
+ APIs /
+ {{ $api.title }} /
+ API {{ $api.version }}
+
+
+
+ {{ if eq .Params.view "overview" }}
+
+ {{ if eq $api.connection "unix" }}
+ User-operated API
+ {{ else }}
+ Hosted API
+ {{ end }}
+
+ {{ $api.title }} API
+
+ API {{ $api.version }} · {{ len $api.operations }} operations ·
+ {{ len $api.schemas }} named schemas
+
+
+ Overview
+
+ {{ partial "api-reference/description.html" (dict "text" $api.description "api" $api) | markdownify }}
+
+
+
+
+ {{ if eq $api.connection "unix" }}
+ Connecting to
+ {{ $api.title }}
+ {{ else }}
+ Connecting to the {{ $api.title }} API
+ {{ end }}
+
+ {{ range $name, $scheme := $api.securitySchemes }}
+ {{ with $scheme.description }}
+ {{ index $scheme "x-displayName" | default $name }}
+ {{ . | markdownify }}
+ {{ end }}
+ {{ end }}
+ {{ if eq $api.connection "unix" }}
+ curl --unix-socket /var/run/docker.sock http://localhost/v{{ $api.version }}/version
+ {{ else }}
+ {{ range $api.servers }}{{ .url }}{{ end }}
+ {{ end }}{{ range $api.guides }}
+ {{ $guide := site.GetPage (index (split . "#") 0) }}
+ {{ $guide.Title }}
+ {{ end }}
+
+
+ {{ $overviewTags := partial "api-reference/overview-tags.html" $api }}
+ {{ range $api.tags }}
+ {{ if not (in $overviewTags .) }}
+
+ {{ end }}
+ {{ end }}
+ {{ range $overviewTags }}
+
+ {{ .summary }}
+
+ {{ partial "api-reference/description.html" (dict "text" .description "api" $api) | markdownify }}
+
+
+ {{ end }}
+
+ Operations
+ Filter operations
+
+
+ Schemas
+
+ {{ range $api.schemas }}
+
+ {{ .name }}
+
+ {{ end }}
+
+ {{ else if eq .Params.view "operation" }}
+ {{ range $api.operations }}
+ {{ if eq .id $.Params.operationID }}
+ {{ .summary }}
+
+ {{ .method }} {{ partial "api-reference/path.html" .path }}{{ if .deprecated }}
+ Deprecated
+ {{ end }}
+
+
+
+
+ {{ partial "api-reference/description.html" (dict "text" .description "api" $api) | markdownify }}
+
+
Connection and access
+
+ API connection and authentication guidance
+
+ {{ if and (eq $api.product "engine") (where .parameters "name" "X-Registry-Auth") }}
+
+ X-Registry-Auth delegates registry credentials
+ and does not authenticate the daemon caller.
+
+ {{ end }}
+ {{ range .servers }}
+
+ {{ .url }}{{ with .description }}
+ —
+ {{ . }}
+ {{ end }}
+
+ {{ end }}
+ {{ if not .security }}
+
+ No HTTP authentication requirement is declared for this
+ operation. Transport access controls can still apply.
+
+ {{ else }}
+
+ Use one of these alternatives. Requirements within an
+ alternative apply together.
+
+
+ {{ range .security }}
+
+ {{ $first := true }}{{ range $name, $scopes := . }}
+ {{ if not $first }}
+ AND
+ {{ end }}{{ $first = false }}{{ $name }}{{ with $scopes }}
+ ({{ delimit . ", " }})
+ {{ end }}
+ {{ end }}{{ if $first }}Anonymous access{{ end }}
+
+ {{ end }}
+
+ {{ end }}
+
Parameters
+ {{ if not .parameters }}
+
No parameters are declared.
+ {{ end }}
+ {{ range .parameters }}
+
+
+ {{ .name }}
+ {{ .in }}
+ {{ if .required }}
+ Required
+ {{ end }}
+
+
+ {{ partial "api-reference/description.html" (dict "text" .description "api" $api) | markdownify }}
+
+ {{ if isset . "schema" }}
+ {{ partial "api-reference/schema.html" (dict "schema" .schema "api" $api "depth" 0) }}
+ {{ end }}{{ with .content }}
+ {{ . | jsonify (dict "indent" " ") }}
+ {{ end }}{{ with .style }}
+ Serialization: {{ . }}
+ {{ end }}
+
+ {{ end }}
+
Request and responses
+
Media type
+
+ All media types
+ {{ $media := slice }}{{ range .variants }}
+ {{ if .media }}
+ {{ $media = $media | append .media }}
+ {{ end }}
+ {{ end }}{{ range ($media | uniq | sort) }}
+ {{ . }}
+ {{ end }}
+
+ {{ range $i, $v := .variants }}
+
+
+ {{ $v.direction | humanize }}
+ {{ with $v.status }}
+ {{ . }}
+ {{ end }}
+
+
+ {{ partial "api-reference/description.html" (dict "text" $v.description "api" $api) | markdownify }}
+
+ {{ if $v.media }}
+ {{ $v.media }}
+ {{ else }}
+ No response content is declared.
+ {{ end }}
+ {{ with $v.headers }}
+ Headers
+ {{ range $name,$header := . }}
+
+
{{ $name }}
+
{{ $header.description }}
+
{{ $header | jsonify (dict "indent" " ") }}
+
+ {{ end }}
+ {{ end }}
+ {{ if isset $v "schema" }}
+ {{ partial "api-reference/schema.html" (dict "schema" $v.schema "api" $api "depth" 0) }}
+ {{ end }}
+ {{ if isset $v "itemSchema" }}
+ Stream item
+ {{ partial "api-reference/schema.html" (dict "schema" $v.itemSchema "api" $api "depth" 0) }}
+ {{ end }}
+ {{ if $v.examples }}
+
+ {{ if gt (len $v.examples) 1 }}
+
Example
+
+ {{ range $j,$ex := $v.examples }}
+ {{ $ex.name }}
+ {{ end }}
+
+ {{ end }}{{ range $j,$ex := $v.examples }}
+
+
{{ $ex.name }}
+
{{ $ex.text }}
+
+ {{ end }}
+
+ {{ end }}
+
+ {{ end }}
+
Referenced schemas
+ {{ range .references }}
+ {{ if .url }}
+
{{ .ref }}
+ {{ end }}
+ {{ end }}
+
+ Complete operation contract
+ {{ .raw | jsonify (dict "indent" " ") }}
+
+
+
+
+
Example request
+ Shell
+
+
+ Replace placeholders and supply the required credentials or
+ request body.
+
+ {{ .curl }}
+ Copy request {{ range .curlNotes }}{{ . }}
{{ end }}
+
+
+ {{ end }}
+ {{ end }}
+ {{ else if eq .Params.view "schema" }}
+ {{ range $api.schemas }}
+ {{ if eq .name $.Params.schemaName }}
+ Schema
+ {{ .name }}
+ {{ partial "api-reference/schema.html" (dict "schema" .schema "api" $api "depth" 0) }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+
+{{ end }}
diff --git a/layouts/api-docs.markdown.md b/layouts/api-docs.markdown.md
new file mode 100644
index 000000000000..5f6e679b27cd
--- /dev/null
+++ b/layouts/api-docs.markdown.md
@@ -0,0 +1,155 @@
+{{- $data := index hugo.Data "api-reference" -}}
+{{- $api := dict -}}{{- range $data.apis -}}{{- if eq .id $.Params.apiID -}}{{- $api = . -}}{{- end -}}{{- end -}}
+# {{ .Title }}
+
+{{ if eq .Params.view "catalog" }}
+Choose a Docker HTTP API:
+{{ range $data.apis }}
+- [{{ .title }} API {{ .version }}]({{ .url }}): {{ len .operations }} operations; {{ .connection }} connection
+{{ end }}
+{{ range $data.legacyAPIs }}
+- [{{ .title }}]({{ .url }}): {{ .description }}
+{{ end }}
+{{ else }}
+[API catalog](/reference/api/) · [{{ $api.title }} overview]({{ $api.url }}) · [Product manual]({{ ref . $api.manual }}) · [OpenAPI specification]({{ $api.sourceURL }})
+
+API version: {{ $api.version }}
+
+{{ if eq .Params.view "overview" }}
+## Overview
+
+{{ partial "api-reference/description.html" (dict "text" $api.description "api" $api) }}
+## {{ if eq $api.connection "unix" }}Connecting to {{ $api.title }}{{ else }}Connecting to the {{ $api.title }} API{{ end }}
+
+{{ range $name, $scheme := $api.securitySchemes }}{{ with $scheme.description }}
+### {{ index $scheme "x-displayName" | default $name }}
+
+{{ . }}
+{{ end }}{{ end }}
+{{ range $api.servers }}
+Server: `{{ .url }}`
+{{ end }}
+{{ if eq $api.connection "unix" }}
+```console
+curl --unix-socket /var/run/docker.sock http://localhost/v{{ $api.version }}/version
+```
+{{ end }}
+{{ range $api.guides }}
+{{ $guide := site.GetPage (index (split . "#") 0) }}
+- [{{ $guide.Title }}]({{ ref $ . }})
+{{ end }}
+
+{{ range (partial "api-reference/overview-tags.html" $api) }}
+## {{ .summary }}
+
+{{ partial "api-reference/description.html" (dict "text" .description "api" $api) }}
+{{ end }}
+## Operations
+{{ range $api.operations }}
+- [{{ .method }} {{ .path }}]({{ .url }}): {{ .summary }}
+{{ end }}
+## Schemas
+{{ range $api.schemas }}
+- [{{ .name }}]({{ .url }})
+{{ end }}
+{{ else if eq .Params.view "operation" }}
+{{ range $api.operations }}{{ if eq .id $.Params.operationID }}
+`{{ .method }} {{ .path }}`
+
+{{ partial "api-reference/description.html" (dict "text" .description "api" $api) }}
+{{ if .deprecated }}
+Deprecated operation.
+{{ end }}
+## Connection and access
+
+[API connection and authentication guidance]({{ $api.url }}#authentication)
+{{ if and (eq $api.product "engine") (where .parameters "name" "X-Registry-Auth") }}
+`X-Registry-Auth` delegates registry credentials and does not authenticate the daemon caller.
+{{ end }}
+{{ range .servers }}
+Server: `{{ .url }}`
+{{ end }}
+Effective security: alternatives are OR; schemes within an alternative are AND. An empty array declares no HTTP authentication requirement.
+
+```json
+{{ .security | jsonify (dict "indent" " ") }}
+```
+## Example request
+
+Replace placeholders and provide the required credentials or request body.
+
+```console
+{{ .curl }}
+```
+{{ range .curlNotes }}
+{{ . }}
+{{ end }}
+## Parameters
+{{ range .parameters }}
+### {{ .name }}
+
+Location: {{ .in }}. Required: {{ if .required }}yes{{ else }}no{{ end }}.
+
+{{ partial "api-reference/description.html" (dict "text" .description "api" $api) }}
+
+```json
+{{ . | jsonify (dict "indent" " ") }}
+```
+{{ end }}
+## Request and responses
+{{ range .variants }}
+### {{ .direction }} {{ .status }} {{ .media }}
+
+{{ partial "api-reference/description.html" (dict "text" .description "api" $api) }}
+{{ if not .media }}
+No response content is declared.
+{{ end }}
+{{ if isset . "schema" }}
+Schema:
+
+```json
+{{ .schema | jsonify (dict "indent" " ") }}
+```
+{{ end }}
+{{ if isset . "itemSchema" }}
+Stream item schema:
+
+```json
+{{ .itemSchema | jsonify (dict "indent" " ") }}
+```
+{{ end }}
+{{ with .headers }}
+Headers:
+
+```json
+{{ . | jsonify (dict "indent" " ") }}
+```
+{{ end }}
+{{ range .examples }}
+{{ .name }}:
+```{{ .language }}
+{{ .text }}
+```
+{{ end }}
+{{ end }}
+## Complete operation contract
+
+```json
+{{ .raw | jsonify (dict "indent" " ") }}
+```
+
+## Referenced schemas
+{{ range .references }}
+- {{ if .url }}[{{ .ref }}]({{ .url }}){{ else }}`{{ .ref }}`{{ end }}
+{{ end }}
+{{ end }}{{ end }}
+{{ else if eq .Params.view "schema" }}
+{{ range $api.schemas }}{{ if eq .name $.Params.schemaName }}
+Schema constraints and annotations:
+
+```json
+{{ .schema | jsonify (dict "indent" " ") }}
+```
+{{ end }}{{ end }}
+{{ end }}
+{{ end }}
diff --git a/layouts/baseof.html b/layouts/baseof.html
index 823d4a087912..87dfff2839fd 100644
--- a/layouts/baseof.html
+++ b/layouts/baseof.html
@@ -2,6 +2,7 @@
{{ partial "head.html" . }}
+ {{ block "head-extra" . }}{{ end }}
=8"
}
},
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
@@ -1728,6 +1751,34 @@
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
"integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="
},
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/delaunator": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
@@ -1736,6 +1787,14 @@
"robust-predicates": "^3.0.2"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -1748,6 +1807,18 @@
"node": ">=0.10"
}
},
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/dompurify": {
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.6.tgz",
@@ -2241,6 +2312,406 @@
"node": ">= 20"
}
},
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -2263,6 +2734,11 @@
"node": ">=4"
}
},
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
diff --git a/package.json b/package.json
index 8cc4272493ba..bb4de3c29c77 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"highlight.js": "11.11.1",
"marked": "17.0.4",
"mermaid": "11.15.0",
+ "micromark": "4.0.2",
"tailwindcss": "4.2.1"
},
"devDependencies": {