mirror of
https://github.com/docker/docs.git
synced 2026-10-23 18:09:02 +03:00
docs: unify Hub, DVP, and Registry API references (#26048)
@netlify /reference/api/ [Preview the API reference](https://deploy-preview-26048--docsdocker.netlify.app/reference/api/) Adds a shared API reference experience for Hub, DVP, and Registry: a catalog, operation navigation, linked schemas, request and response examples, and matching HTML and Markdown output. A validation and generation pipeline prepares the reference data for Hugo and runs as part of Docker and Netlify builds. Migrates the three specifications to OpenAPI 3.2, correcting response shapes, nullable fields, and authentication and token examples. The Hub migration also corrects SCIM contracts and CSV exports; DVP gains complete report response schemas and examples. Registry keeps its documented Hub-specific subset, with authentication and media negotiation expressed through OpenAPI security and content fields. Validation is strict for both checks and generation, with no exception baseline or bypass flag. The build uses the migrated sources directly, without migration scripts or records. Page and specification URLs remain stable, including the DVP alias. Operation and schema anchors change. Engine and Governance retain their existing references. Validated locally with Go tests, strict validation of all three specifications (zero diagnostics), scoped formatting and lint, and a full Hugo build checking all 176 HTML/Markdown page pairs, example parity, reference links, and byte-identical published specifications. A browser check confirmed raw CSV rendering and the generated request header. No live API requests were made.
This commit is contained in:
@@ -7,11 +7,9 @@ IgnoreAltEmpty: true
|
||||
IgnoreEmptyHref: true
|
||||
IgnoreDirectoryMissingTrailingSlash: true
|
||||
IgnoreURLs:
|
||||
- "^/reference/api/hub/.*$"
|
||||
- "^/reference/api/engine/v.+/#.*$"
|
||||
- "^/reference/api/registry/.*$"
|
||||
- "^/pagefind/.*$"
|
||||
- "^/reference/api/engine/v.+/#.*$"
|
||||
- "^/pagefind/.*$"
|
||||
IgnoreDirs:
|
||||
- "registry/configuration"
|
||||
- "compose/compose-file" # temporarily ignore until upstream is fixed
|
||||
- "registry/configuration"
|
||||
- "compose/compose-file" # temporarily ignore until upstream is fixed
|
||||
CacheExpires: "6h"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -424,3 +424,4 @@ superset
|
||||
tokenization
|
||||
WebGL
|
||||
Xubuntu
|
||||
Netlify
|
||||
|
||||
596
assets/api-reference/reference.css
Normal file
596
assets/api-reference/reference.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
49
assets/api-reference/reference.js
Normal file
49
assets/api-reference/reference.js
Normal file
@@ -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,
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
13
compose.yaml
13
compose.yaml
@@ -11,3 +11,16 @@ services:
|
||||
- action: sync
|
||||
path: .
|
||||
target: /project
|
||||
ignore:
|
||||
- hack/api-docs/
|
||||
- content/reference/api/hub/latest.yaml
|
||||
- content/reference/api/dvp/latest.yaml
|
||||
- content/reference/api/registry/latest.yaml
|
||||
- action: rebuild
|
||||
path: hack/api-docs
|
||||
- action: rebuild
|
||||
path: content/reference/api/hub/latest.yaml
|
||||
- action: rebuild
|
||||
path: content/reference/api/dvp/latest.yaml
|
||||
- action: rebuild
|
||||
path: content/reference/api/registry/latest.yaml
|
||||
|
||||
@@ -38,7 +38,7 @@ organization.
|
||||
{{< /tab >}}
|
||||
{{< tab name="API">}}
|
||||
|
||||
To view activity logs using the Docker Hub API, use the [Audit logs endpoints](https://docs.docker.com/reference/api/hub/latest/#tag/audit-logs).
|
||||
To view activity logs using the Docker Hub API, use the [Audit logs endpoints](/reference/api/hub/latest/#tag-audit-logs).
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
@@ -49,7 +49,7 @@ To view activity logs using the Docker Hub API, use the [Audit logs endpoints](h
|
||||
>
|
||||
> Docker Home retains activity logs for 30 days. To retrieve
|
||||
activities beyond 30 days, you must use the
|
||||
[Docker Hub API](https://docs.docker.com/reference/api/hub/latest/#tag/audit-logs).
|
||||
[Docker Hub API](/reference/api/hub/latest/#tag-audit-logs).
|
||||
|
||||
By default, the **Activity** tab displays all recorded events within
|
||||
the last 30 days. To narrow your view, use the calendar to select a specific
|
||||
|
||||
@@ -258,7 +258,7 @@ After you invite and manage members, explore these related topics:
|
||||
|
||||
[roles-permissions]: /manuals/security/roles-and-permissions/_index.md
|
||||
[licenses-and-invites]: /manuals/accounts/organization/manage/manage-licenses.md#licenses-and-invites
|
||||
[bulk-invites]: /reference/api/hub/latest/#tag/invites/paths/~1v2~1invites~1bulk/post
|
||||
[bulk-invites]: /reference/api/hub/latest/operations/postV2InvitesBulk/
|
||||
[docker-pricing]: https://www.docker.com/pricing?ref=Docs&refAction=DocsAdminMembers
|
||||
[scim-role-mapping]: /manuals/security/provisioning/scim/_index.md
|
||||
[scim-provisioning]: /manuals/security/provisioning/scim/_index.md
|
||||
|
||||
@@ -207,7 +207,7 @@ The updated [Docker Subscription Service Agreement](https://www.docker.com/legal
|
||||
|
||||
When managing the content of your repositories, you can now filter the results based on the currentness of the tags and more easily identify your untagged images.
|
||||
|
||||
For Docker Hub API documentation, see [Docker Hub API Reference](/reference/api/hub/latest.md#operation/GetNamespacesRepositoriesImages).
|
||||
For Docker Hub API documentation, see [Docker Hub API Reference](/reference/api/hub/latest/).
|
||||
|
||||
## 2021-04-13
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ invitations are not supported for this role.
|
||||
To invite distributor members:
|
||||
|
||||
1. Use the [authentication
|
||||
API](https://docs.docker.com/reference/api/hub/latest/#tag/authentication-api/operation/AuthCreateAccessToken)
|
||||
API](/reference/api/hub/latest/operations/AuthCreateAccessToken/)
|
||||
to generate a bearer token for your Docker Hub account. This token authorizes
|
||||
the API requests you use to send invites. Replace `myusername` and
|
||||
`dckr_pat_...` with your Docker ID and a [personal access
|
||||
@@ -213,7 +213,7 @@ To invite distributor members:
|
||||
{{< tab name="API" >}}
|
||||
|
||||
Use the [teams
|
||||
API](https://docs.docker.com/reference/api/hub/latest/#tag/groups/paths/~1v2~1orgs~1%7Borg_name%7D~1groups/post):
|
||||
API](/reference/api/hub/latest/operations/postV2OrgsByOrgNameGroups/):
|
||||
|
||||
```console
|
||||
$ curl -s -X POST "https://hub.docker.com/v2/orgs/example-org/groups" \
|
||||
@@ -245,7 +245,7 @@ To invite distributor members:
|
||||
{{< tab name="API" >}}
|
||||
|
||||
Use the [repository teams
|
||||
API](https://docs.docker.com/reference/api/hub/latest/#tag/repositories/operation/CreateRepositoryGroup),
|
||||
API](/reference/api/hub/latest/operations/CreateRepositoryGroup/),
|
||||
passing the team's `id` from the previous step as `group_id`:
|
||||
|
||||
```console
|
||||
@@ -259,7 +259,7 @@ To invite distributor members:
|
||||
{{< /tabs >}}
|
||||
|
||||
4. Use the [bulk create invites
|
||||
endpoint](https://docs.docker.com/reference/api/hub/latest/#tag/invites/paths/~1v2~1invites~1bulk/post)
|
||||
endpoint](/reference/api/hub/latest/operations/postV2InvitesBulk/)
|
||||
to send email invites. In the request body, set `role` to
|
||||
`distributor_member`, specify the `team`, and list the invitees' email
|
||||
addresses:
|
||||
|
||||
@@ -28,7 +28,7 @@ The following objects are shown in the diagram.
|
||||
|
||||
Use the following steps to delete one or more items via the graphical user
|
||||
interface. To delete in bulk, see the [deletion API
|
||||
endpoint](/reference/api/registry/latest/#tag/delete).
|
||||
endpoint](/reference/api/registry/latest/#tag-delete).
|
||||
|
||||
> [!NOTE]
|
||||
> Images associated with [immutable tags](./immutable-tags.md) can't be
|
||||
|
||||
@@ -192,11 +192,11 @@ allowed`, regardless of the token's scopes. Use the linked replacement
|
||||
endpoints instead:
|
||||
|
||||
- `GET /v2/repositories/{namespace}/{repository}` — use
|
||||
[Get repository](/reference/api/hub/latest/#tag/repositories/operation/GetRepository).
|
||||
[Get repository](/reference/api/hub/latest/operations/GetRepository/).
|
||||
- `GET /v2/repositories/{namespace}` — use
|
||||
[List repositories](/reference/api/hub/latest/#tag/repositories/operation/listNamespaceRepositories).
|
||||
[List repositories](/reference/api/hub/latest/operations/listNamespaceRepositories/).
|
||||
- `GET /v2/users/{username}/repositories` — use
|
||||
[List repositories](/reference/api/hub/latest/#tag/repositories/operation/listNamespaceRepositories).
|
||||
[List repositories](/reference/api/hub/latest/operations/listNamespaceRepositories/).
|
||||
|
||||
## Organization access token best practices
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Docker supports the following SCIM attributes:
|
||||
| `active` | Indicates if a user is enabled or disabled, set to "false" to de-provision a user |
|
||||
|
||||
For additional details about supported attributes and SCIM, see
|
||||
[Docker Hub API SCIM reference](/reference/api/hub/latest.md#tag/scim).
|
||||
[Docker Hub API SCIM reference](/reference/api/hub/latest/#tag-scim).
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
|
||||
22
content/reference/api/_content.gotmpl
Normal file
22
content/reference/api/_content.gotmpl
Normal file
@@ -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" "weight" 1 "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 -}}
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
title: API reference
|
||||
build:
|
||||
render: never
|
||||
---
|
||||
|
||||
@@ -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/
|
||||
---
|
||||
@@ -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: |
|
||||
<SchemaDefinition schemaRef="#/components/schemas/ResponseDataFile" />
|
||||
[ResponseDataFile](#schema-ResponseDataFile)
|
||||
summary: ResponseDataFile
|
||||
kind: info
|
||||
- name: yearModel
|
||||
x-displayName: Year Data Model
|
||||
description: |
|
||||
<SchemaDefinition schemaRef="#/components/schemas/YearModel" />
|
||||
[YearModel](#schema-YearModel)
|
||||
summary: Year Data Model
|
||||
kind: info
|
||||
- name: monthModel
|
||||
x-displayName: Month Data Model
|
||||
description: |
|
||||
<SchemaDefinition schemaRef="#/components/schemas/MonthModel" />
|
||||
[MonthModel](#schema-MonthModel)
|
||||
summary: Month Data Model
|
||||
kind: info
|
||||
- name: weekModel
|
||||
x-displayName: Week Data Model
|
||||
description: |
|
||||
<SchemaDefinition schemaRef="#/components/schemas/WeekModel" />
|
||||
|
||||
[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,142 +594,306 @@ 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: |
|
||||
JWT Bearer Authentication is required to access the Docker DVP Data API.
|
||||
|
||||
This authentication documentation is duplicated from the [Hub API Authentication docs](https://docs.docker.com/reference/api/hub/latest/#tag/authentication)
|
||||
This authentication documentation is duplicated from the [Hub API Authentication docs](https://docs.docker.com/reference/api/hub/latest/#authentication)
|
||||
x-displayName: Docker Hub Authentication
|
||||
jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
|
||||
x-features-openapi:
|
||||
schemaDefinitionsTagName: Schemas
|
||||
|
||||
@@ -5,5 +5,5 @@ title: Latest
|
||||
params:
|
||||
sidebar:
|
||||
goto: /reference/api/engine/latest/
|
||||
weight: 20
|
||||
weight: 1
|
||||
---
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: API reference by version
|
||||
weight: 4
|
||||
build:
|
||||
render: never
|
||||
sidebar:
|
||||
|
||||
@@ -19,12 +19,12 @@ issues for Docker Service APIs.
|
||||
|
||||
### Updates
|
||||
|
||||
- Document that [Update org member (role)](/reference/api/hub/latest/#tag/orgs)
|
||||
- Document that [Update org member (role)](/reference/api/hub/latest/#tag-orgs)
|
||||
accepts a custom role name in `role`.
|
||||
- Document the `role` field on
|
||||
[Update some details for an organization group](/reference/api/hub/latest/#tag/groups).
|
||||
[Update some details for an organization group](/reference/api/hub/latest/#tag-groups).
|
||||
- Add `Editor` to `org_member.role` on
|
||||
[organization members](/reference/api/hub/latest/#tag/orgs).
|
||||
[organization members](/reference/api/hub/latest/#tag-orgs).
|
||||
|
||||
---
|
||||
|
||||
@@ -33,9 +33,9 @@ issues for Docker Service APIs.
|
||||
### Updates
|
||||
|
||||
- Document organization access token (OAT) support for
|
||||
[repository management](/reference/api/hub/latest/#tag/repositories)
|
||||
[repository management](/reference/api/hub/latest/#tag-repositories)
|
||||
endpoints, the scope-dependent behavior of
|
||||
[List repositories](/reference/api/hub/latest/#tag/repositories/operation/listNamespaceRepositories),
|
||||
[List repositories](/reference/api/hub/latest/operations/listNamespaceRepositories/),
|
||||
and legacy paths that are OAT unsupported.
|
||||
|
||||
---
|
||||
@@ -44,13 +44,13 @@ issues for Docker Service APIs.
|
||||
|
||||
### Updates
|
||||
|
||||
- Add missing `expires_at` fields on [PAT management](/reference/api/hub/latest/#tag/access-tokens) endpoints.
|
||||
- Add missing `expires_at` fields on [PAT management](/reference/api/hub/latest/#tag-access-tokens) endpoints.
|
||||
|
||||
## 2025-09-25
|
||||
|
||||
### Updates
|
||||
|
||||
- Fix [Assign repository group](/reference/api/hub/latest/#tag/repositories/operation/CreateRepositoryGroup) endpoints request/response
|
||||
- Fix [Assign repository group](/reference/api/hub/latest/operations/CreateRepositoryGroup/) endpoints request/response
|
||||
|
||||
---
|
||||
|
||||
@@ -58,16 +58,16 @@ issues for Docker Service APIs.
|
||||
|
||||
### New
|
||||
|
||||
- Add [Create repository](/reference/api/hub/latest/#tag/repositories/operation/CreateRepository) endpoints for a given `namespace`.
|
||||
- Add [Get repository](/reference/api/hub/latest/#tag/repositories/operation/GetRepository) endpoints for a given `namespace`.
|
||||
- Add [Check repository](/reference/api/hub/latest/#tag/repositories/operation/CheckRepository) endpoints for a given `namespace`.
|
||||
- Add [Create repository](/reference/api/hub/latest/operations/CreateRepository/) endpoints for a given `namespace`.
|
||||
- Add [Get repository](/reference/api/hub/latest/operations/GetRepository/) endpoints for a given `namespace`.
|
||||
- Add [Check repository](/reference/api/hub/latest/operations/CheckRepository/) endpoints for a given `namespace`.
|
||||
|
||||
### Deprecations
|
||||
|
||||
- [Deprecate POST /v2/repositories](/reference/api/hub/deprecated/#deprecate-legacy-createrepository)
|
||||
- [Deprecate POST /v2/repositories/{namespace}](/reference/api/hub/deprecated/#deprecate-legacy-createrepository)
|
||||
- [Deprecate GET /v2/repositories/{namespace}/{repository}](/reference/api/hub/deprecated/#deprecate-legacy-getrepository)
|
||||
- [Deprecate HEAD /v2/repositories/{namespace}/{repository}](/reference/api/hub/deprecated/#deprecate-legacy-getrepository)
|
||||
- [Deprecate POST /v2/repositories](/reference/api/hub/deprecated/#deprecate-legacy-createrepository-and-getrepository)
|
||||
- [Deprecate POST /v2/repositories/{namespace}](/reference/api/hub/deprecated/#deprecate-legacy-createrepository-and-getrepository)
|
||||
- [Deprecate GET /v2/repositories/{namespace}/{repository}](/reference/api/hub/deprecated/#deprecate-legacy-createrepository-and-getrepository)
|
||||
- [Deprecate HEAD /v2/repositories/{namespace}/{repository}](/reference/api/hub/deprecated/#deprecate-legacy-createrepository-and-getrepository)
|
||||
|
||||
---
|
||||
|
||||
@@ -75,8 +75,8 @@ issues for Docker Service APIs.
|
||||
|
||||
### New
|
||||
|
||||
- Add [Update repository immutable tags settings](/reference/api/hub/latest/#tag/repositories/operation/UpdateRepositoryImmutableTags) endpoints for a given `namespace` and `repository`.
|
||||
- Add [Verify repository immutable tags](/reference/api/hub/latest/#tag/repositories/operation/VerifyRepositoryImmutableTags) endpoints for a given `namespace` and `repository`.
|
||||
- Add [Update repository immutable tags settings](/reference/api/hub/latest/operations/UpdateRepositoryImmutableTags/) endpoints for a given `namespace` and `repository`.
|
||||
- Add [Verify repository immutable tags](/reference/api/hub/latest/operations/VerifyRepositoryImmutableTags/) endpoints for a given `namespace` and `repository`.
|
||||
|
||||
---
|
||||
|
||||
@@ -84,7 +84,7 @@ issues for Docker Service APIs.
|
||||
|
||||
### New
|
||||
|
||||
- Add [List repositories](/reference/api/hub/latest/#tag/repositories/operation/listNamespaceRepositories) endpoints for a given `namespace`.
|
||||
- Add [List repositories](/reference/api/hub/latest/operations/listNamespaceRepositories/) endpoints for a given `namespace`.
|
||||
|
||||
### Deprecations
|
||||
|
||||
@@ -96,7 +96,7 @@ issues for Docker Service APIs.
|
||||
|
||||
### New
|
||||
|
||||
- Add [APIs](/reference/api/hub/latest/#tag/org-access-tokens) for organization access token (OATs) management.
|
||||
- Add [APIs](/reference/api/hub/latest/#tag-org-access-tokens) for organization access token (OATs) management.
|
||||
|
||||
---
|
||||
|
||||
@@ -104,5 +104,5 @@ issues for Docker Service APIs.
|
||||
|
||||
### New
|
||||
|
||||
- Add access to [audit logs](/reference/api/hub/latest/#tag/audit-logs) for org
|
||||
- Add access to [audit logs](/reference/api/hub/latest/#tag-audit-logs) for org
|
||||
access tokens.
|
||||
|
||||
@@ -38,15 +38,15 @@ The endpoint may be removed, disabled, or change behavior in a future release.
|
||||
### Deprecate legacy CreateRepository and GetRepository
|
||||
|
||||
Deprecate undocumented endpoints :
|
||||
- `POST /v2/repositories` and `POST /v2/repositories/{namespace}` replaced by [Create repository](/reference/api/hub/latest/#tag/repositories/operation/CreateRepository).
|
||||
- `GET /v2/repositories/{namespace}/{repository}` replaced by [Get repository](/reference/api/hub/latest/#tag/repositories/operation/GetRepository).
|
||||
- `HEAD /v2/repositories/{namespace}/{repository}` replaced by [Check repository](/reference/api/hub/latest/#tag/repositories/operation/CheckRepository).
|
||||
- `POST /v2/repositories` and `POST /v2/repositories/{namespace}` replaced by [Create repository](/reference/api/hub/latest/operations/CreateRepository/).
|
||||
- `GET /v2/repositories/{namespace}/{repository}` replaced by [Get repository](/reference/api/hub/latest/operations/GetRepository/).
|
||||
- `HEAD /v2/repositories/{namespace}/{repository}` replaced by [Check repository](/reference/api/hub/latest/operations/CheckRepository/).
|
||||
|
||||
---
|
||||
|
||||
### Deprecate legacy ListNamespaceRepositories
|
||||
|
||||
Deprecate undocumented endpoint `GET /v2/repositories/{namespace}` replaced by [List repositories](/reference/api/hub/latest/#tag/repositories/operation/listNamespaceRepositories).
|
||||
Deprecate undocumented endpoint `GET /v2/repositories/{namespace}` replaced by [List repositories](/reference/api/hub/latest/operations/listNamespaceRepositories/).
|
||||
|
||||
---
|
||||
|
||||
@@ -72,9 +72,9 @@ If you want to continue using the Docker Hub API in your current applications, u
|
||||
|
||||
| **OLD** | **NEW** |
|
||||
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [/v1/repositories/{name}/tags](https://github.com/moby/moby/blob/v1.8.3/docs/reference/api/registry_api.md#list-repository-tags) | [/v2/namespaces/{namespace}/repositories/{repository}/tags](/reference/api/hub/latest/#tag/repositories/operation/ListRepositoryTags) |
|
||||
| [/v1/repositories/{namespace}/{name}/tags](https://github.com/moby/moby/blob/v1.8.3/docs/reference/api/registry_api.md#list-repository-tags) | [/v2/namespaces/{namespace}/repositories/{repository}/tags](/reference/api/hub/latest.md/#tag/repositories/operation/ListRepositoryTags) |
|
||||
| [/v1/repositories/{namespace}/{name}/tags](https://github.com/moby/moby/blob/v1.8.3/docs/reference/api/registry_api.md#get-image-id-for-a-particular-tag) | [/v2/namespaces/{namespace}/repositories/{repository}/tags/{tag}](/reference/api/hub/latest/#tag/repositories/operation/GetRepositoryTag) |
|
||||
| [/v1/repositories/{namespace}/{name}/tags/{tag_name}](https://github.com/moby/moby/blob/v1.8.3/docs/reference/api/registry_api.md#get-image-id-for-a-particular-tag) | [/v2/namespaces/{namespace}/repositories/{repository}/tags/{tag}](/reference/api/hub/latest/#tag/repositories/operation/GetRepositoryTag) |
|
||||
| [/v1/repositories/{name}/tags](https://github.com/moby/moby/blob/v1.8.3/docs/reference/api/registry_api.md#list-repository-tags) | [/v2/namespaces/{namespace}/repositories/{repository}/tags](/reference/api/hub/latest/operations/ListRepositoryTags/) |
|
||||
| [/v1/repositories/{namespace}/{name}/tags](https://github.com/moby/moby/blob/v1.8.3/docs/reference/api/registry_api.md#list-repository-tags) | [/v2/namespaces/{namespace}/repositories/{repository}/tags](/reference/api/hub/latest/operations/ListRepositoryTags/) |
|
||||
| [/v1/repositories/{namespace}/{name}/tags](https://github.com/moby/moby/blob/v1.8.3/docs/reference/api/registry_api.md#get-image-id-for-a-particular-tag) | [/v2/namespaces/{namespace}/repositories/{repository}/tags/{tag}](/reference/api/hub/latest/operations/GetRepositoryTag/) |
|
||||
| [/v1/repositories/{namespace}/{name}/tags/{tag_name}](https://github.com/moby/moby/blob/v1.8.3/docs/reference/api/registry_api.md#get-image-id-for-a-particular-tag) | [/v2/namespaces/{namespace}/repositories/{repository}/tags/{tag}](/reference/api/hub/latest/operations/GetRepositoryTag/) |
|
||||
|
||||
---
|
||||
@@ -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
|
||||
---
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Registry authentication
|
||||
weight: 10
|
||||
description: "Specifies the Docker Registry v2 authentication"
|
||||
keywords: registry, images, tags, repository, distribution, Bearer authentication, advanced
|
||||
---
|
||||
@@ -218,4 +219,4 @@ token placed in the HTTP `Authorization` header like so:
|
||||
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IkJWM0Q6MkFWWjpVQjVaOktJQVA6SU5QTDo1RU42Ok40SjQ6Nk1XTzpEUktFOkJWUUs6M0ZKTDpQT1RMIn0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJCQ0NZOk9VNlo6UUVKNTpXTjJDOjJBVkM6WTdZRDpBM0xZOjQ1VVc6NE9HRDpLQUxMOkNOSjU6NUlVTCIsImF1ZCI6InJlZ2lzdHJ5LmRvY2tlci5jb20iLCJleHAiOjE0MTUzODczMTUsIm5iZiI6MTQxNTM4NzAxNSwiaWF0IjoxNDE1Mzg3MDE1LCJqdGkiOiJ0WUpDTzFjNmNueXk3a0FuMGM3cktQZ2JWMUgxYkZ3cyIsInNjb3BlIjoiamxoYXduOnJlcG9zaXRvcnk6c2FtYWxiYS9teS1hcHA6cHVzaCxwdWxsIGpsaGF3bjpuYW1lc3BhY2U6c2FtYWxiYTpwdWxsIn0.Y3zZSwaZPqy4y9oRBVRImZyv3m_S9XDHF1tWwN7mL52C_IiA73SJkWVNsvNqpJIn5h7A2F8biv_S2ppQ1lgkbw
|
||||
```
|
||||
|
||||
This is also described in [Section 2.1 of RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://tools.ietf.org/html/rfc6750#section-2.1)
|
||||
This is also described in [Section 2.1 of RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://tools.ietf.org/html/rfc6750#section-2.1)
|
||||
|
||||
@@ -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
|
||||
---
|
||||
@@ -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.
|
||||
|
||||
|
||||
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 <<EOF
|
||||
{
|
||||
@@ -187,14 +184,14 @@ tags:
|
||||
-H "Content-Type: application/vnd.docker.distribution.manifest.v2+json" \
|
||||
-d "$MANIFEST" \
|
||||
https://registry-1.docker.io/v2/$REPO/manifests/$TAG
|
||||
|
||||
|
||||
echo "Pushed image to $REPO:$TAG"
|
||||
```
|
||||
|
||||
This example pushes a minimal image with no layers. To push a complete image, repeat steps 2–3 for each layer and include the layer digests in the `layers[]` field of the manifest.
|
||||
|
||||
summary: Pushing Images
|
||||
kind: info
|
||||
- name: delete
|
||||
x-displayName: Deleting Images
|
||||
description: |
|
||||
Deleting an image involves removing its manifest by digest. You must first retrieve the manifest digest, then issue a `DELETE` request using that digest.
|
||||
|
||||
@@ -215,36 +212,36 @@ tags:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
USERNAME=yourusername
|
||||
PASSWORD=dckr_pat
|
||||
REPO=yourusername/helloworld
|
||||
TAG=latest
|
||||
|
||||
|
||||
# Step 1: Get a bearer token
|
||||
TOKEN=$(curl -s -u "$USERNAME:$PASSWORD" \
|
||||
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:$REPO:pull,push,delete" \
|
||||
| jq -r .token)
|
||||
|
||||
|
||||
# Step 2 and 3: Get the manifest and extract the digest from response headers
|
||||
DIGEST=$(curl -sI -H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
|
||||
https://registry-1.docker.io/v2/$REPO/manifests/$TAG \
|
||||
| grep -i Docker-Content-Digest | tr -d '\r' | awk '{print $2}')
|
||||
|
||||
|
||||
echo "Deleting manifest with digest: $DIGEST"
|
||||
|
||||
|
||||
# Step 4: Delete the manifest by digest
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
https://registry-1.docker.io/v2/$REPO/manifests/$DIGEST
|
||||
|
||||
|
||||
echo "Deleted image: $REPO@$DIGEST"
|
||||
```
|
||||
|
||||
|
||||
This example deletes the manifest for the `latest` tag. To fully delete all references to an image, ensure no other tags or referrers point to the same manifest digest.
|
||||
|
||||
|
||||
summary: Deleting Images
|
||||
kind: info
|
||||
paths:
|
||||
/v2/{name}/manifests/{reference}:
|
||||
get:
|
||||
@@ -259,7 +256,13 @@ paths:
|
||||
The manifest contains metadata about the image, including configuration and layer digests. It is required for pulling images from the registry.
|
||||
|
||||
This endpoint requires authentication. Use the `Authorization: Bearer <token>` 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 <token>`).
|
||||
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 <token>`).
|
||||
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 <token>
|
||||
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: "<binary data not shown>"
|
||||
|
||||
value: <binary data not shown>
|
||||
"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: "<binary data not shown>"
|
||||
|
||||
value: <binary data not shown>
|
||||
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: "<binary data not shown>"
|
||||
|
||||
value: <binary data not shown>
|
||||
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: []
|
||||
|
||||
87
hack/api-docs/README.md
Normal file
87
hack/api-docs/README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# 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.
|
||||
With `docker compose watch`, changes to the registered specifications or
|
||||
`hack/api-docs/` rebuild the server image and regenerate the reference.
|
||||
|
||||
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. It also checks local links and fragments from API pages,
|
||||
including links to `docs.docker.com`. The site `htmltest` checks include migrated
|
||||
Hub, DVP, and Registry references. `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.
|
||||
106
hack/api-docs/browser-checks.mjs
Normal file
106
hack/api-docs/browser-checks.mjs
Normal file
@@ -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;
|
||||
}
|
||||
128
hack/api-docs/contracts_test.go
Normal file
128
hack/api-docs/contracts_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
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/createOrgAccessTokenRequest", `{"label":"CI token"}`, false},
|
||||
{"/components/schemas/createOrgAccessTokenRequest", `{"label":"CI token","resources":[]}`, false},
|
||||
{"/components/schemas/createOrgAccessTokenRequest", `{"label":"CI token","resources":[{"type":"TYPE_REPO","path":"myorg/myrepo","scopes":["scope-image-pull"]}]}`, true},
|
||||
{"/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", "<html>&\n", "<html>&\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)
|
||||
}
|
||||
}
|
||||
}
|
||||
18
hack/api-docs/go.mod
Normal file
18
hack/api-docs/go.mod
Normal file
@@ -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
|
||||
)
|
||||
30
hack/api-docs/go.sum
Normal file
30
hack/api-docs/go.sum
Normal file
@@ -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=
|
||||
761
hack/api-docs/main.go
Normal file
761
hack/api-docs/main.go
Normal file
@@ -0,0 +1,761 @@
|
||||
// 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 {
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
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")
|
||||
}
|
||||
for i, requirement := range arr(op["security"]) {
|
||||
for _, name := range keys(obj(requirement)) {
|
||||
if _, ok := obj(obj(d.Root["components"])["securitySchemes"])[name]; !ok {
|
||||
d.issue("security", fmt.Sprintf("%s/security/%d/%s", p, i, esc(name)), "Undefined security scheme: "+name)
|
||||
}
|
||||
}
|
||||
}
|
||||
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 (d *Document) 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(d.resolve(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
|
||||
}
|
||||
354
hack/api-docs/main_test.go
Normal file
354
hack/api-docs/main_test.go
Normal file
@@ -0,0 +1,354 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUndefinedSecurityRequirements(t *testing.T) {
|
||||
for _, inherited := range []bool{false, true} {
|
||||
d := fixture(t, "valid.yaml")
|
||||
requirements := []any{Object{"bearerAuth": []any{}}, Object{"missingAuth": []any{}}}
|
||||
if inherited {
|
||||
d.Root["security"] = requirements
|
||||
} else {
|
||||
obj(obj(obj(d.Root["paths"])["/public"])["get"])["security"] = requirements
|
||||
}
|
||||
d.validate("")
|
||||
found := false
|
||||
for _, diagnostic := range d.Diagnostics {
|
||||
if diagnostic.Rule == "security" && strings.Contains(diagnostic.Message, "missingAuth") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("undefined security accepted (inherited=%v)", inherited)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferencedMediaExample(t *testing.T) {
|
||||
d := fixture(t, "valid.yaml")
|
||||
obj(d.Root["components"])["examples"] = Object{"Flag": Object{"value": false}}
|
||||
media := obj(obj(obj(obj(obj(d.Root["paths"])["/public"])["get"])["responses"])["200"])
|
||||
v := obj(obj(media["content"])["application/json"])
|
||||
delete(v, "example")
|
||||
v["examples"] = Object{"flag": Object{"$ref": "#/components/examples/Flag"}}
|
||||
d.validate("")
|
||||
if len(d.Diagnostics) != 0 {
|
||||
t.Fatalf("referenced example rejected: %v", d.Diagnostics)
|
||||
}
|
||||
examples := d.mediaExamples(v)
|
||||
if len(examples) != 1 || obj(examples[0])["value"] != false {
|
||||
t.Fatalf("referenced false example lost: %#v", examples)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalDynamicReference(t *testing.T) {
|
||||
original, err := os.ReadFile("testdata/valid.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
if err := os.Mkdir(filepath.Join(dir, "schemas"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
event, err := os.ReadFile("testdata/schemas/event.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "schemas/event.yaml"), event, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file := filepath.Join(dir, "api.yaml")
|
||||
if err := os.WriteFile(file, []byte(strings.Replace(string(original), "$ref: ./schemas/event.yaml", "$dynamicRef: ./schemas/event.yaml", 1)), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := loadDocument(file, "validation/dialects")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d.validate("")
|
||||
if len(d.Diagnostics) != 0 {
|
||||
t.Fatalf("external dynamic reference rejected: %v", d.Diagnostics)
|
||||
}
|
||||
}
|
||||
293
hack/api-docs/model.go
Normal file
293
hack/api-docs/model.go
Normal file
@@ -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://<SERVER>"
|
||||
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 := d.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
|
||||
}
|
||||
44
hack/api-docs/run.sh
Executable file
44
hack/api-docs/run.sh
Executable file
@@ -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
|
||||
48
hack/api-docs/sources.json
Normal file
48
hack/api-docs/sources.json
Normal file
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
102
hack/api-docs/testdata/duplicate-key.yaml
vendored
Normal file
102
hack/api-docs/testdata/duplicate-key.yaml
vendored
Normal file
@@ -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'
|
||||
101
hack/api-docs/testdata/invalid-example.yaml
vendored
Normal file
101
hack/api-docs/testdata/invalid-example.yaml
vendored
Normal file
@@ -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'
|
||||
100
hack/api-docs/testdata/invalid-info.yaml
vendored
Normal file
100
hack/api-docs/testdata/invalid-info.yaml
vendored
Normal file
@@ -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'
|
||||
101
hack/api-docs/testdata/invalid-item-type.yaml
vendored
Normal file
101
hack/api-docs/testdata/invalid-item-type.yaml
vendored
Normal file
@@ -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'
|
||||
101
hack/api-docs/testdata/invalid-stream-ref.yaml
vendored
Normal file
101
hack/api-docs/testdata/invalid-stream-ref.yaml
vendored
Normal file
@@ -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'
|
||||
100
hack/api-docs/testdata/profile-missing-id.yaml
vendored
Normal file
100
hack/api-docs/testdata/profile-missing-id.yaml
vendored
Normal file
@@ -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'
|
||||
9
hack/api-docs/testdata/schemas/event.yaml
vendored
Normal file
9
hack/api-docs/testdata/schemas/event.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
type: object
|
||||
required:
|
||||
- event
|
||||
- data
|
||||
properties:
|
||||
event:
|
||||
type: string
|
||||
data:
|
||||
type: string
|
||||
110
hack/api-docs/testdata/valid.yaml
vendored
Normal file
110
hack/api-docs/testdata/valid.yaml
vendored
Normal file
@@ -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"
|
||||
20
hack/api-docs/validation/dialects/lock.json
Normal file
20
hack/api-docs/validation/dialects/lock.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
25
hack/api-docs/validation/dialects/oas-dialect.json
Normal file
25
hack/api-docs/validation/dialects/oas-dialect.json
Normal file
@@ -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" }
|
||||
]
|
||||
}
|
||||
1666
hack/api-docs/validation/dialects/oas-document.json
Normal file
1666
hack/api-docs/validation/dialects/oas-document.json
Normal file
File diff suppressed because it is too large
Load Diff
87
hack/api-docs/validation/dialects/oas-vocabulary.json
Normal file
87
hack/api-docs/validation/dialects/oas-vocabulary.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
24
hack/api-docs/validation/rules.yaml
Normal file
24
hack/api-docs/validation/rules.yaml
Normal file
@@ -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
|
||||
176
hack/api-docs/verify-output.mjs
Normal file
176
hack/api-docs/verify-output.mjs
Normal file
@@ -0,0 +1,176 @@
|
||||
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 local links and fragments in generated API pages, including 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;
|
||||
const article = html.match(
|
||||
/<article\b[^>]*class=["']?api-reference[\s\S]*?<\/article>/,
|
||||
)?.[0];
|
||||
if (!article) {
|
||||
problems.push(`Missing API article: ${path.relative(base, file)}`);
|
||||
continue;
|
||||
}
|
||||
for (const href of attributes(article, "href")) {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(href, "http://localhost:1314/" + path.relative(base, file));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!["localhost", "docs.docker.com"].includes(url.hostname) ||
|
||||
!["http:", "https:"].includes(url.protocol)
|
||||
)
|
||||
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}`);
|
||||
} else if (url.hash && target.endsWith(".html")) {
|
||||
const targetHTML = fs.readFileSync(target, "utf8");
|
||||
const anchors = [
|
||||
...attributes(targetHTML, "id"),
|
||||
...attributes(targetHTML, "name"),
|
||||
];
|
||||
if (!anchors.includes(decodeURIComponent(url.hash.slice(1))))
|
||||
problems.push(
|
||||
`Broken API anchor: ${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("<redoc") || html.includes("data-api-view="))
|
||||
problems.push(`Legacy Engine renderer changed: ${url}`);
|
||||
}
|
||||
if (problems.length)
|
||||
throw Error(
|
||||
problems.slice(0, 25).join("\n") + `\n${problems.length} output failures`,
|
||||
);
|
||||
const bytes = files(path.join(base, "reference/api")).reduce(
|
||||
(n, p) => 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.`,
|
||||
);
|
||||
@@ -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("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const publicDir = path.resolve(process.argv[2] || "public");
|
||||
|
||||
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);
|
||||
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 });
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
return results;
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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})`;
|
||||
}
|
||||
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)),
|
||||
);
|
||||
|
||||
// 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++;
|
||||
}
|
||||
|
||||
console.log(`Flattened ${count} index.md files`);
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, '/');
|
||||
|
||||
const content = rewriteLinks(original, events, file);
|
||||
if (content !== original) {
|
||||
fs.writeFileSync(file, content, 'utf8');
|
||||
count++;
|
||||
fs.writeFileSync(file, content);
|
||||
rewritten++;
|
||||
}
|
||||
if (
|
||||
path.basename(file) === "index.md" &&
|
||||
file !== path.join(publicDir, "index.md")
|
||||
) {
|
||||
fs.renameSync(file, path.dirname(file) + ".md");
|
||||
flattened++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Fixed _index.md references in ${count} files`);
|
||||
return count;
|
||||
console.log(
|
||||
`Markdown: rewrote links in ${rewritten} files; flattened ${flattened} index.md files`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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++;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
119
hack/test/flatten-and-resolve.mjs
Normal file
119
hack/test/flatten-and-resolve.mjs
Normal file
@@ -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](<file.md> "A title")',
|
||||
'[Title](</guide/section/file/> "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 });
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
|
||||
13
layouts/_partials/api-reference/description.html
Normal file
13
layouts/_partials/api-reference/description.html
Normal file
@@ -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 -}}
|
||||
11
layouts/_partials/api-reference/manual-link.html
Normal file
11
layouts/_partials/api-reference/manual-link.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{{ $data := index hugo.Data "api-reference" }}
|
||||
{{ range $data.apis }}
|
||||
{{ $manual := site.GetPage .manual }}
|
||||
{{ if and $manual (eq $manual.RelPermalink $.RelPermalink) }}
|
||||
<p
|
||||
class="my-4 rounded border border-blue-200 bg-blue-50 p-4 text-sm text-blue-900"
|
||||
>
|
||||
<a href="{{ .url }}">Explore the {{ .title }} API {{ .version }} →</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
36
layouts/_partials/api-reference/navigation.html
Normal file
36
layouts/_partials/api-reference/navigation.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{{ $data := index hugo.Data "api-reference" }}
|
||||
<nav class="api-nav" aria-label="API navigation">
|
||||
<a class="api-nav-back" href="/reference/api/">← API catalog</a>
|
||||
{{ range $data.apis }}
|
||||
{{ if eq .id $.Params.apiID }}
|
||||
<h2>{{ .title }}</h2>
|
||||
<p class="api-nav-version">API {{ .version }}</p>
|
||||
<a
|
||||
href="{{ .url }}"
|
||||
{{ if eq $.Params.view "overview" }}
|
||||
aria-current="page" id="sidebar-current-page"
|
||||
{{ end }}
|
||||
>Overview</a
|
||||
>
|
||||
{{ range .tags }}
|
||||
{{ if eq .kind "nav" }}
|
||||
{{ $tag := .name }}
|
||||
<h3>{{ .summary }}</h3>
|
||||
{{ range (index (where $data.apis "id" $.Params.apiID) 0).operations }}
|
||||
{{ if eq (index .tags 0) $tag }}
|
||||
<a
|
||||
class="api-nav-operation"
|
||||
href="{{ .url }}"
|
||||
{{ if eq .id $.Params.operationID }}
|
||||
aria-current="page" id="sidebar-current-page"
|
||||
{{ end }}
|
||||
><small data-method="{{ .method }}">{{ .method }}</small
|
||||
><span>{{ .summary }}</span></a
|
||||
>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
11
layouts/_partials/api-reference/overview-tags.html
Normal file
11
layouts/_partials/api-reference/overview-tags.html
Normal file
@@ -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 -}}
|
||||
4
layouts/_partials/api-reference/path.html
Normal file
4
layouts/_partials/api-reference/path.html
Normal file
@@ -0,0 +1,4 @@
|
||||
{{- /* Break long paths between segments without changing their copied text. */ -}}
|
||||
{{- range $i, $segment := split . "/" -}}
|
||||
{{- if $i -}}<wbr />/{{- end -}}{{- $segment -}}
|
||||
{{- end -}}
|
||||
85
layouts/_partials/api-reference/schema.html
Normal file
85
layouts/_partials/api-reference/schema.html
Normal file
@@ -0,0 +1,85 @@
|
||||
{{ $s := .schema }}{{ $api := .api }}{{ $depth := .depth }}
|
||||
{{ if reflect.IsMap $s }}
|
||||
{{ with index $s "$ref" }}
|
||||
{{ $url := index $api.schemaURLs . }}
|
||||
<p class="api-ref">
|
||||
Schema:
|
||||
{{ if $url }}
|
||||
<a href="{{ $url }}"
|
||||
><code>{{ strings.TrimPrefix "#/components/schemas/" . }}</code></a
|
||||
>
|
||||
{{ else }}
|
||||
<code>{{ . }}</code>
|
||||
{{ end }}
|
||||
</p>
|
||||
{{ end }}
|
||||
{{ with $s.type }}
|
||||
<p class="api-type">
|
||||
Type:
|
||||
<code
|
||||
>{{ if reflect.IsSlice . }}
|
||||
{{ delimit . " | " }}
|
||||
{{ else }}
|
||||
{{ . }}
|
||||
{{ end }}</code
|
||||
>
|
||||
</p>
|
||||
{{ end }}
|
||||
{{ with $s.description }}
|
||||
<div class="prose dark:prose-invert">{{ . | markdownify }}</div>
|
||||
{{ end }}
|
||||
{{ if lt $depth 3 }}
|
||||
{{ range $name,$v := $s.properties }}
|
||||
<details class="api-field" {{ if eq $depth 0 }}open{{ end }}>
|
||||
<summary>
|
||||
<code>{{ $name }}</code>{{ if in $s.required $name }}
|
||||
· required
|
||||
{{ end }}{{ if $v.readOnly }}
|
||||
· response only
|
||||
{{ end }}{{ if $v.writeOnly }}· request only{{ end }}
|
||||
</summary>
|
||||
{{ partial "api-reference/schema.html" (dict "schema" $v "api" $api "depth" (add $depth 1)) }}
|
||||
</details>
|
||||
{{ end }}
|
||||
{{ range $keyword := slice "allOf" "oneOf" "anyOf" }}
|
||||
{{ with index $s $keyword }}
|
||||
<details class="api-field" open>
|
||||
<summary>
|
||||
{{ 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 }}
|
||||
</summary>
|
||||
{{ range . }}
|
||||
{{ partial "api-reference/schema.html" (dict "schema" . "api" $api "depth" (add $depth 1)) }}
|
||||
{{ end }}
|
||||
</details>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if isset $s "items" }}
|
||||
<details class="api-field">
|
||||
<summary>Array item</summary>
|
||||
{{ partial "api-reference/schema.html" (dict "schema" $s.items "api" $api "depth" (add $depth 1)) }}
|
||||
</details>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if or (ge $depth 3) (not (index $s "$ref")) (gt (len $s) 1) }}
|
||||
<details class="api-constraints">
|
||||
<summary>All schema constraints and annotations</summary>
|
||||
<pre><code>{{ $s | jsonify (dict "indent" " ") }}</code></pre>
|
||||
</details>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<p>
|
||||
{{ 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 }}
|
||||
</p>
|
||||
{{ end }}
|
||||
@@ -12,6 +12,7 @@
|
||||
{{ partialCached "pagemeta.html" . . }}
|
||||
<hr />
|
||||
</div>
|
||||
{{ partial "api-reference/manual-link.html" . }}
|
||||
{{ .Content }}
|
||||
{{- if and (eq .Type "guides") (not .IsSection) (ne .CurrentSection .FirstSection) -}}
|
||||
{{- with .PrevInSection -}}
|
||||
|
||||
@@ -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 @@
|
||||
{{ . }}
|
||||
</li>
|
||||
{{- range where $pages "Params.sidebar.group" . }}
|
||||
{{- if .IsSection }}
|
||||
{{- if and .IsSection (not .Params.apiID) }}
|
||||
{{- template "renderList" (dict "section" . "revealHidden" $revealHidden) }}
|
||||
{{- else }}
|
||||
{{- template "renderSingle" . }}
|
||||
|
||||
369
layouts/api-docs.html
Normal file
369
layouts/api-docs.html
Normal file
@@ -0,0 +1,369 @@
|
||||
{{ define "head-extra" }}
|
||||
{{ $style := resources.Get "api-reference/reference.css" | fingerprint }}
|
||||
<link rel="stylesheet" href="{{ $style.RelPermalink }}" />
|
||||
{{ $script := resources.Get "api-reference/reference.js" | fingerprint }}
|
||||
<script src="{{ $script.RelPermalink }}" defer></script>
|
||||
{{ 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" }}
|
||||
<article class="api-reference" data-api-view="{{ .Params.view }}">
|
||||
{{ $data := index hugo.Data "api-reference" }}
|
||||
{{ $api := dict }}{{ range $data.apis }}
|
||||
{{ if eq .id $.Params.apiID }}{{ $api = . }}{{ end }}
|
||||
{{ end }}
|
||||
{{ if eq .Params.view "catalog" }}
|
||||
<p class="api-eyebrow">Developer reference</p>
|
||||
<h1>Docker APIs</h1>
|
||||
<p class="api-lead">
|
||||
Build with Docker, from your local daemon to hosted services.
|
||||
</p>
|
||||
<p>
|
||||
Choose an API to find connection guidance, operations, and data models.
|
||||
</p>
|
||||
<div class="api-cards">
|
||||
{{ range $data.apis }}
|
||||
<a class="api-card" href="{{ .url }}"
|
||||
><span class="api-label"
|
||||
>{{ if eq .connection "unix" }}
|
||||
Your daemon
|
||||
{{ else }}
|
||||
Hosted service
|
||||
{{ end }}</span
|
||||
>
|
||||
<h2>{{ .title }}</h2>
|
||||
<p>API {{ .version }} · {{ len .operations }} operations</p>
|
||||
<span>Explore reference →</span></a
|
||||
>
|
||||
{{ end }}
|
||||
{{ range $data.legacyAPIs }}
|
||||
<a class="api-card" href="{{ .url }}">
|
||||
<span class="api-label"
|
||||
>{{ if eq .connection "unix" }}
|
||||
Your daemon
|
||||
{{ else }}
|
||||
Hosted service
|
||||
{{ end }}</span
|
||||
>
|
||||
<h2>{{ .title }}</h2>
|
||||
<p>{{ .description }}</p>
|
||||
<span>Explore reference →</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
<nav class="api-crumbs" aria-label="API breadcrumb">
|
||||
<a href="/reference/api/">APIs</a> /
|
||||
<a href="{{ $api.url }}">{{ $api.title }}</a> /
|
||||
<span>API {{ $api.version }}</span>
|
||||
</nav>
|
||||
<div class="api-tools">
|
||||
<label
|
||||
>API version
|
||||
<select data-api-version>
|
||||
{{ range $data.apis }}
|
||||
{{ if eq .product $api.product }}
|
||||
<option
|
||||
value="{{ .url }}"
|
||||
{{ if eq .id $api.id }}selected{{ end }}
|
||||
>
|
||||
{{ .version }}
|
||||
</option>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</select></label
|
||||
><a href="{{ ref . $api.manual }}">Product manual</a
|
||||
><a href="{{ $api.sourceURL }}">Download OpenAPI specification</a
|
||||
><a href="{{ partial "utils/markdown-url.html" . }}">Markdown</a>
|
||||
</div>
|
||||
<meta
|
||||
data-pagefind-meta="title:{{ .Title }} — {{ $api.title }} API {{ $api.version }}"
|
||||
/>
|
||||
{{ if eq .Params.view "overview" }}
|
||||
<p class="api-eyebrow">
|
||||
{{ if eq $api.connection "unix" }}
|
||||
User-operated API
|
||||
{{ else }}
|
||||
Hosted API
|
||||
{{ end }}
|
||||
</p>
|
||||
<h1>{{ $api.title }} API</h1>
|
||||
<p class="api-lead">
|
||||
API {{ $api.version }} · {{ len $api.operations }} operations ·
|
||||
{{ len $api.schemas }} named schemas
|
||||
</p>
|
||||
<section class="api-overview" aria-labelledby="api-overview-title">
|
||||
<h2 id="api-overview-title">Overview</h2>
|
||||
<div class="prose dark:prose-invert">
|
||||
{{ partial "api-reference/description.html" (dict "text" $api.description "api" $api) | markdownify }}
|
||||
</div>
|
||||
</section>
|
||||
<section class="api-connection" id="authentication">
|
||||
<h2>
|
||||
{{ if eq $api.connection "unix" }}
|
||||
Connecting to
|
||||
{{ $api.title }}
|
||||
{{ else }}
|
||||
Connecting to the {{ $api.title }} API
|
||||
{{ end }}
|
||||
</h2>
|
||||
{{ range $name, $scheme := $api.securitySchemes }}
|
||||
{{ with $scheme.description }}
|
||||
<h3>{{ index $scheme "x-displayName" | default $name }}</h3>
|
||||
<div class="prose dark:prose-invert">{{ . | markdownify }}</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if eq $api.connection "unix" }}
|
||||
<pre><code>curl --unix-socket /var/run/docker.sock http://localhost/v{{ $api.version }}/version</code></pre>
|
||||
{{ else }}
|
||||
{{ range $api.servers }}<code>{{ .url }}</code>{{ end }}
|
||||
{{ end }}{{ range $api.guides }}
|
||||
{{ $guide := site.GetPage (index (split . "#") 0) }}
|
||||
<p><a href="{{ ref $ . }}">{{ $guide.Title }}</a></p>
|
||||
{{ end }}
|
||||
</section>
|
||||
<div class="api-overview">
|
||||
{{ $overviewTags := partial "api-reference/overview-tags.html" $api }}
|
||||
{{ range $api.tags }}
|
||||
{{ if not (in $overviewTags .) }}
|
||||
<span id="tag-{{ .name | urlize }}"></span>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ range $overviewTags }}
|
||||
<section id="tag-{{ .name | urlize }}">
|
||||
<h2>{{ .summary }}</h2>
|
||||
<div class="prose dark:prose-invert">
|
||||
{{ partial "api-reference/description.html" (dict "text" .description "api" $api) | markdownify }}
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
||||
</div>
|
||||
<h2>Operations</h2>
|
||||
<label class="api-filter-label"
|
||||
>Filter operations
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Method, path, or operation name"
|
||||
data-api-filter
|
||||
/></label>
|
||||
<div class="api-operation-list">
|
||||
{{ range $api.operations }}
|
||||
<a class="api-operation-row" href="{{ .url }}" data-api-filter-item
|
||||
><span class="api-method" data-method="{{ .method }}"
|
||||
>{{ .method }}</span
|
||||
><code>{{ partial "api-reference/path.html" .path }}</code
|
||||
><span>{{ .summary }}</span></a
|
||||
>
|
||||
{{ end }}
|
||||
</div>
|
||||
<h2 id="schemas">Schemas</h2>
|
||||
<ul class="api-schema-links" aria-label="Schemas">
|
||||
{{ range $api.schemas }}
|
||||
<li>
|
||||
<a href="{{ .url }}"><code>{{ .name }}</code></a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else if eq .Params.view "operation" }}
|
||||
{{ range $api.operations }}
|
||||
{{ if eq .id $.Params.operationID }}
|
||||
<h1>{{ .summary }}</h1>
|
||||
<div class="api-signature">
|
||||
<span class="api-method" data-method="{{ .method }}"
|
||||
>{{ .method }}</span
|
||||
><code>{{ partial "api-reference/path.html" .path }}</code
|
||||
>{{ if .deprecated }}
|
||||
<span>Deprecated</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="api-operation-grid">
|
||||
<div class="api-reading">
|
||||
<div class="api-description prose dark:prose-invert">
|
||||
{{ partial "api-reference/description.html" (dict "text" .description "api" $api) | markdownify }}
|
||||
</div>
|
||||
<h2>Connection and access</h2>
|
||||
<p>
|
||||
<a href="{{ $api.url }}#authentication"
|
||||
>API connection and authentication guidance</a
|
||||
>
|
||||
</p>
|
||||
{{ if and (eq $api.product "engine") (where .parameters "name" "X-Registry-Auth") }}
|
||||
<p>
|
||||
<code>X-Registry-Auth</code> delegates registry credentials
|
||||
and does not authenticate the daemon caller.
|
||||
</p>
|
||||
{{ end }}
|
||||
{{ range .servers }}
|
||||
<p>
|
||||
<code>{{ .url }}</code>{{ with .description }}
|
||||
—
|
||||
{{ . }}
|
||||
{{ end }}
|
||||
</p>
|
||||
{{ end }}
|
||||
{{ if not .security }}
|
||||
<p>
|
||||
No HTTP authentication requirement is declared for this
|
||||
operation. Transport access controls can still apply.
|
||||
</p>
|
||||
{{ else }}
|
||||
<p>
|
||||
Use one of these alternatives. Requirements within an
|
||||
alternative apply together.
|
||||
</p>
|
||||
<ul>
|
||||
{{ range .security }}
|
||||
<li>
|
||||
{{ $first := true }}{{ range $name, $scopes := . }}
|
||||
{{ if not $first }}
|
||||
AND
|
||||
{{ end }}{{ $first = false }}<code>{{ $name }}</code>{{ with $scopes }}
|
||||
({{ delimit . ", " }})
|
||||
{{ end }}
|
||||
{{ end }}{{ if $first }}Anonymous access{{ end }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
<h2>Parameters</h2>
|
||||
{{ if not .parameters }}
|
||||
<p>No parameters are declared.</p>
|
||||
{{ end }}
|
||||
{{ range .parameters }}
|
||||
<section class="api-field" data-api-parameter="{{ .name }}">
|
||||
<h3>
|
||||
<code>{{ .name }}</code>
|
||||
<small>{{ .in }}</small>
|
||||
{{ if .required }}
|
||||
<small class="api-required">Required</small>
|
||||
{{ end }}
|
||||
</h3>
|
||||
<div class="prose dark:prose-invert">
|
||||
{{ partial "api-reference/description.html" (dict "text" .description "api" $api) | markdownify }}
|
||||
</div>
|
||||
{{ if isset . "schema" }}
|
||||
{{ partial "api-reference/schema.html" (dict "schema" .schema "api" $api "depth" 0) }}
|
||||
{{ end }}{{ with .content }}
|
||||
<pre><code>{{ . | jsonify (dict "indent" " ") }}</code></pre>
|
||||
{{ end }}{{ with .style }}
|
||||
<p>Serialization: <code>{{ . }}</code></p>
|
||||
{{ end }}
|
||||
</section>
|
||||
{{ end }}
|
||||
<h2>Request and responses</h2>
|
||||
<label
|
||||
>Media type
|
||||
<select data-api-media-select>
|
||||
<option value="">All media types</option>
|
||||
{{ $media := slice }}{{ range .variants }}
|
||||
{{ if .media }}
|
||||
{{ $media = $media | append .media }}
|
||||
{{ end }}
|
||||
{{ end }}{{ range ($media | uniq | sort) }}
|
||||
<option value="{{ . }}">{{ . }}</option>
|
||||
{{ end }}
|
||||
</select></label
|
||||
>
|
||||
{{ range $i, $v := .variants }}
|
||||
<section
|
||||
class="api-variant"
|
||||
data-api-variant="{{ $v.pointer }}"
|
||||
data-api-media="{{ $v.media }}"
|
||||
>
|
||||
<h3>
|
||||
<span>{{ $v.direction | humanize }}</span>
|
||||
{{ with $v.status }}
|
||||
<code class="api-status">{{ . }}</code>
|
||||
{{ end }}
|
||||
</h3>
|
||||
<div class="prose dark:prose-invert">
|
||||
{{ partial "api-reference/description.html" (dict "text" $v.description "api" $api) | markdownify }}
|
||||
</div>
|
||||
{{ if $v.media }}
|
||||
<p class="api-media"><code>{{ $v.media }}</code></p>
|
||||
{{ else }}
|
||||
<p>No response content is declared.</p>
|
||||
{{ end }}
|
||||
{{ with $v.headers }}
|
||||
<h4>Headers</h4>
|
||||
{{ range $name,$header := . }}
|
||||
<div class="api-field">
|
||||
<code>{{ $name }}</code>
|
||||
<p>{{ $header.description }}</p>
|
||||
<pre><code>{{ $header | jsonify (dict "indent" " ") }}</code></pre>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if isset $v "schema" }}
|
||||
{{ partial "api-reference/schema.html" (dict "schema" $v.schema "api" $api "depth" 0) }}
|
||||
{{ end }}
|
||||
{{ if isset $v "itemSchema" }}
|
||||
<h4>Stream item</h4>
|
||||
{{ partial "api-reference/schema.html" (dict "schema" $v.itemSchema "api" $api "depth" 0) }}
|
||||
{{ end }}
|
||||
{{ if $v.examples }}
|
||||
<div data-api-examples>
|
||||
{{ if gt (len $v.examples) 1 }}
|
||||
<label
|
||||
>Example
|
||||
<select data-api-example-select>
|
||||
{{ range $j,$ex := $v.examples }}
|
||||
<option value="{{ $j }}">{{ $ex.name }}</option>
|
||||
{{ end }}
|
||||
</select></label
|
||||
>
|
||||
{{ end }}{{ range $j,$ex := $v.examples }}
|
||||
<div data-api-example="{{ $j }}">
|
||||
<h4>{{ $ex.name }}</h4>
|
||||
<pre><code>{{ $ex.text }}</code></pre>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</section>
|
||||
{{ end }}
|
||||
<h2>Referenced schemas</h2>
|
||||
{{ range .references }}
|
||||
{{ if .url }}
|
||||
<p><a href="{{ .url }}">{{ .ref }}</a></p>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<details>
|
||||
<summary>Complete operation contract</summary>
|
||||
<pre><code>{{ .raw | jsonify (dict "indent" " ") }}</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
<aside class="api-request">
|
||||
<div class="api-request-heading">
|
||||
<h2>Example request</h2>
|
||||
<span>Shell</span>
|
||||
</div>
|
||||
<p>
|
||||
Replace placeholders and supply the required credentials or
|
||||
request body.
|
||||
</p>
|
||||
<pre><code data-api-copy-source>{{ .curl }}</code></pre>
|
||||
<button type="button" data-api-copy>Copy request</button
|
||||
>{{ range .curlNotes }}<p>{{ . }}</p>{{ end }}
|
||||
</aside>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ else if eq .Params.view "schema" }}
|
||||
{{ range $api.schemas }}
|
||||
{{ if eq .name $.Params.schemaName }}
|
||||
<p class="api-eyebrow">Schema</p>
|
||||
<h1>{{ .name }}</h1>
|
||||
{{ partial "api-reference/schema.html" (dict "schema" .schema "api" $api "depth" 0) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</article>
|
||||
{{ end }}
|
||||
155
layouts/api-docs.markdown.md
Normal file
155
layouts/api-docs.markdown.md
Normal file
@@ -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 }}
|
||||
@@ -2,6 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
{{ partial "head.html" . }}
|
||||
{{ block "head-extra" . }}{{ end }}
|
||||
</head>
|
||||
|
||||
<body
|
||||
|
||||
@@ -3,6 +3,7 @@ publish = "public"
|
||||
|
||||
[context.deploy-preview.environment]
|
||||
NODE_VERSION = "24"
|
||||
GO_VERSION = "1.26.5"
|
||||
NODE_ENV = "production"
|
||||
HUGO_VERSION = "0.163.0"
|
||||
HUGO_ENABLEGITINFO = "true"
|
||||
@@ -10,4 +11,4 @@ HUGO_ENVIRONMENT = "preview"
|
||||
SECRETS_SCAN_OMIT_PATHS = "public/contribute/file-conventions/index.html"
|
||||
|
||||
[context.deploy-preview]
|
||||
command = "hugo --gc --minify -b $DEPLOY_PRIME_URL && ./hack/flatten-and-resolve.js && npx pagefind@v1.5.2"
|
||||
command = "./hack/api-docs/run.sh generate && hugo --gc --minify -b $DEPLOY_PRIME_URL && node hack/flatten-and-resolve.js && node hack/api-docs/verify-output.mjs && npx pagefind@v1.5.2"
|
||||
|
||||
476
package-lock.json
generated
476
package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"highlight.js": "11.11.1",
|
||||
"marked": "17.0.4",
|
||||
"mermaid": "11.15.0",
|
||||
"micromark": "4.0.2",
|
||||
"tailwindcss": "4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1173,11 +1174,24 @@
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
||||
"integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
@@ -1229,6 +1243,15 @@
|
||||
"node": ">=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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user