1
0
mirror of https://github.com/docker/docs.git synced 2026-10-23 18:09:02 +03:00
Files
docs/hack/api-docs/main_test.go
David Karlsson bed22bda81 docs: Cloud Sandboxes API and SDK (#26168)
## Description

Add an experimental Sandboxes API and SDK section alongside Docker
Sandboxes under AI and agents, at `/ai/sandboxes-api/`. Cover
installation of `@docker/sandboxes`, browser and PAT authentication, and
a TypeScript walkthrough that launches the shell kit, runs a command,
and deletes the sandbox. Explain kits and image choices with examples,
along with compute sizes, quotas, permissions, resource lifecycle,
errors, and retries. Link subscription setup and billing to the shared
Docker Agentic Platform guide. Preserve the previous
`/ai/sandboxes/api/` URLs with redirects.

Uses the shared Docker Agentic Platform documentation merged in #26142.
Sibling to #26158, which introduces CLI-side Cloud Sandboxes
documentation.

Include 36 SDK cookbook recipes at `/ai/sandboxes-api/cookbook/` and the
public REST API reference at `/reference/api/sandboxes/`. Import the
generated export from `docker/sbx-api` commit `60d79c1d` (source
`1a7f4026`). Overview and installation remain maintained here. The
reference uses the shared API renderer from main, with separate
operation and schema pages, streaming and WebSocket details, and a
downloadable OpenAPI spec. An API reference link appears in the API/SDK
sidebar. A pinned manifest and import script provide the basis for later
release automation.

Examples are optional in the shared renderer; supplied examples still
have to pass schema validation. Requests without body examples use a
request-body file with instructions to prepare it. The import copies all
selected recipes and the OpenAPI specification byte for byte from the
corrected upstream export, without patches or link rewrites. The broken
naming recipe remains temporarily excluded.

This remains a draft pending validation against the published npm SDK.
The imported cookbook contains the TypeScript-only guidance, five-minute
request deadlines, kit-readiness explanation, active-policy status
requirement, and snapshot-deletion and lifetime-renewal restrictions.

Validation: scoped Markdown and Vale checks, full Hugo build, rendered
navigation, redirects, and internal links, and TypeScript type-checking
against the generated public SDK from `docker/sbx-api` commit
`68f214b9`. Import validation also covers all 60 REST operations and 188
schema definitions, 1,198 imported-page links and anchors,
desktop/mobile rendering, Markdown output, spec delivery, and a repeat
import. The shared renderer tests, strict generation for all five APIs
(zero diagnostics), full Hugo build, and verification of 453
HTML/Markdown page pairs and API links pass. No live sandbox execution.
The corrected export passes strict generation for all five APIs with
zero diagnostics, scoped Markdown/Vale lint, a full Hugo build, and
verification of 453 HTML/Markdown page pairs and API links. A repeat
import produces identical files. The spec update changes examples only;
schemas and operations are unchanged.

@netlify /ai/sandboxes-api/

Generated by Codex

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-24 18:06:30 +02:00

430 lines
14 KiB
Go

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 TestSequentialMediaEncodingPreserved(t *testing.T) {
d := fixture(t, "valid.yaml")
op := obj(obj(obj(d.Root["paths"])["/events"])["get"])
response := obj(obj(op["responses"])["200"])
encoding := Object{"contentType": "application/json", "headers": Object{
"Content-Disposition": Object{"schema": Object{"type": "string"}},
}}
response["content"] = Object{"multipart/mixed": Object{
"itemSchema": true, "example": jsonNumber("0"), "itemEncoding": encoding,
}}
d.validate("")
if len(d.Diagnostics) != 0 {
t.Fatalf("sequential media fixture must be valid: %+v", d.Diagnostics)
}
variants := d.variants(op, "/paths/~1events/get")
if len(variants) != 1 || !reflect.DeepEqual(obj(variants[0])["itemEncoding"], encoding) {
t.Fatalf("stream content types and part headers were lost: %#v", variants)
}
}
func TestWebSocketDoesNotProducePlainHTTPRequest(t *testing.T) {
op := Object{"method": "GET", "path": "/interact", "raw": Object{
"x-websocket": Object{"subprotocol": "sandboxes.v1"},
}}
command, notes := curlExample(Source{}, op)
if command != "" || len(notes) == 0 || !strings.Contains(notes[0], "WebSocket") {
t.Fatalf("WebSocket operation must describe its client requirements, not a plain HTTP request: %q %v", command, notes)
}
}
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)
}
}
func TestExamplesAreOptionalButValidatedWhenPresent(t *testing.T) {
d := fixture(t, "valid.yaml")
var remove func(any)
remove = func(value any) {
switch v := value.(type) {
case map[string]any:
delete(v, "example")
delete(v, "examples")
for _, child := range v {
remove(child)
}
case []any:
for _, child := range v {
remove(child)
}
}
}
remove(d.Root)
d.validate("")
if len(d.Diagnostics) != 0 {
t.Fatalf("missing examples must be accepted: %+v", d.Diagnostics)
}
invalid := fixture(t, "invalid-example.yaml")
invalid.validate("")
for _, diagnostic := range invalid.Diagnostics {
if diagnostic.Rule == "example" {
return
}
}
t.Fatal("invalid supplied example must still fail")
}
func TestRequestWithoutExampleUsesBodyFile(t *testing.T) {
op := Object{"method": "POST", "path": "/jobs", "servers": []any{Object{"url": "https://api.test"}},
"variants": []any{Object{"direction": "Request", "media": "application/json", "required": true, "examples": []any{}}},
}
command, notes := curlExample(Source{}, op)
if !strings.Contains(command, "--data-binary @request-body") || strings.Contains(command, "--data-raw") {
t.Fatalf("request must use a body file, not an invented payload: %s", command)
}
if !strings.Contains(strings.Join(notes, " "), "Prepare request-body") {
t.Fatalf("missing body instructions: %v", notes)
}
}